Merge branch 'master' into feature-circles
This commit is contained in:
commit
824d1b8893
906 changed files with 37100 additions and 11600 deletions
|
@ -16,17 +16,49 @@ describe AccountFollowController do
|
|||
allow(service).to receive(:call)
|
||||
end
|
||||
|
||||
it 'does not create for user who is not signed in' do
|
||||
subject
|
||||
expect(FollowService).not_to receive(:new)
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
alice.deletion_request.destroy
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
it 'redirects to account path' do
|
||||
sign_in(user)
|
||||
subject
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
subject
|
||||
end
|
||||
|
||||
expect(service).to have_received(:call).with(user.account, alice, with_rate_limit: true)
|
||||
expect(response).to redirect_to(account_path(alice))
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed out' do
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it 'does not follow' do
|
||||
expect(FollowService).not_to receive(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
subject
|
||||
end
|
||||
|
||||
it 'redirects to account path' do
|
||||
expect(service).to have_received(:call).with(user.account, alice, with_rate_limit: true)
|
||||
expect(response).to redirect_to(account_path(alice))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,17 +16,49 @@ describe AccountUnfollowController do
|
|||
allow(service).to receive(:call)
|
||||
end
|
||||
|
||||
it 'does not create for user who is not signed in' do
|
||||
subject
|
||||
expect(UnfollowService).not_to receive(:new)
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
alice.deletion_request.destroy
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
it 'redirects to account path' do
|
||||
sign_in(user)
|
||||
subject
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
subject
|
||||
end
|
||||
|
||||
expect(service).to have_received(:call).with(user.account, alice)
|
||||
expect(response).to redirect_to(account_path(alice))
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed out' do
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it 'does not unfollow' do
|
||||
expect(UnfollowService).not_to receive(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
subject
|
||||
end
|
||||
|
||||
it 'redirects to account path' do
|
||||
expect(service).to have_received(:call).with(user.account, alice)
|
||||
expect(response).to redirect_to(account_path(alice))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,21 @@ RSpec.describe AccountsController, type: :controller do
|
|||
|
||||
let(:account) { Fabricate(:user).account }
|
||||
|
||||
shared_examples 'cachable response' do
|
||||
it 'does not set cookies' do
|
||||
expect(response.cookies).to be_empty
|
||||
expect(response.headers['Set-Cookies']).to be nil
|
||||
end
|
||||
|
||||
it 'does not set sessions' do
|
||||
expect(session).to be_empty
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
let(:format) { 'html' }
|
||||
|
||||
|
@ -33,10 +48,17 @@ RSpec.describe AccountsController, type: :controller do
|
|||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is suspended' do
|
||||
context 'as HTML' do
|
||||
let(:format) { 'html' }
|
||||
|
||||
it_behaves_like 'preliminary checks'
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
|
@ -44,12 +66,17 @@ RSpec.describe AccountsController, type: :controller do
|
|||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'as HTML' do
|
||||
let(:format) { 'html' }
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
end
|
||||
|
||||
it_behaves_like 'preliminary checks'
|
||||
it 'returns http forbidden' do
|
||||
get :show, params: { username: account.username, format: format }
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'common response characteristics' do
|
||||
it 'returns http success' do
|
||||
|
@ -310,6 +337,29 @@ RSpec.describe AccountsController, type: :controller do
|
|||
|
||||
it_behaves_like 'preliminary checks'
|
||||
|
||||
context 'when account is suspended permanently' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
get :show, params: { username: account.username, format: format }
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is suspended temporarily' do
|
||||
before do
|
||||
account.suspend!
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
get :show, params: { username: account.username, format: format }
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
before do
|
||||
get :show, params: { username: account.username, format: format }
|
||||
|
@ -323,9 +373,7 @@ RSpec.describe AccountsController, type: :controller do
|
|||
expect(response.content_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
it_behaves_like 'cachable response'
|
||||
|
||||
it 'renders account' do
|
||||
json = body_as_json
|
||||
|
@ -335,26 +383,8 @@ RSpec.describe AccountsController, type: :controller do
|
|||
context 'in authorized fetch mode' do
|
||||
let(:authorized_fetch_mode) { true }
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'returns application/activity+json' do
|
||||
expect(response.content_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
|
||||
it 'returns Vary header with Signature' do
|
||||
expect(response.headers['Vary']).to include 'Signature'
|
||||
end
|
||||
|
||||
it 'renders bare minimum account' do
|
||||
json = body_as_json
|
||||
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey)
|
||||
expect(json).to_not include(:name, :summary)
|
||||
it 'returns http unauthorized' do
|
||||
expect(response).to have_http_status(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -401,9 +431,7 @@ RSpec.describe AccountsController, type: :controller do
|
|||
expect(response.content_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
it_behaves_like 'cachable response'
|
||||
|
||||
it 'renders account' do
|
||||
json = body_as_json
|
||||
|
@ -442,14 +470,35 @@ RSpec.describe AccountsController, type: :controller do
|
|||
|
||||
it_behaves_like 'preliminary checks'
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
get :show, params: { username: account.username, format: format }
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
get :show, params: { username: account.username, format: format }
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'common response characteristics' do
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
it_behaves_like 'cachable response'
|
||||
end
|
||||
|
||||
context do
|
||||
|
|
|
@ -6,6 +6,22 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
|
|||
let!(:account) { Fabricate(:account) }
|
||||
let(:remote_account) { nil }
|
||||
|
||||
shared_examples 'cachable response' do
|
||||
it 'does not set cookies' do
|
||||
expect(response.cookies).to be_empty
|
||||
expect(response.headers['Set-Cookies']).to be nil
|
||||
end
|
||||
|
||||
it 'does not set sessions' do
|
||||
response
|
||||
expect(session).to be_empty
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:signed_request_account).and_return(remote_account)
|
||||
|
||||
|
@ -19,9 +35,8 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
|
|||
context 'without signature' do
|
||||
let(:remote_account) { nil }
|
||||
|
||||
before do
|
||||
get :show, params: { id: 'featured', account_username: account.username }
|
||||
end
|
||||
subject(:response) { get :show, params: { id: 'featured', account_username: account.username } }
|
||||
subject(:body) { body_as_json }
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
|
@ -31,14 +46,32 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
|
|||
expect(response.content_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
it_behaves_like 'cachable response'
|
||||
|
||||
it 'returns orderedItems with pinned statuses' do
|
||||
json = body_as_json
|
||||
expect(json[:orderedItems]).to be_an Array
|
||||
expect(json[:orderedItems].size).to eq 2
|
||||
expect(body[:orderedItems]).to be_an Array
|
||||
expect(body[:orderedItems].size).to eq 2
|
||||
end
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -58,9 +91,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
|
|||
expect(response.content_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
it_behaves_like 'cachable response'
|
||||
|
||||
it 'returns orderedItems with pinned statuses' do
|
||||
json = body_as_json
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controller do
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:follower_1) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/a') }
|
||||
let!(:follower_2) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/b') }
|
||||
let!(:follower_3) { Fabricate(:account, domain: 'foo.com', uri: 'https://foo.com/users/a') }
|
||||
|
||||
before do
|
||||
follower_1.follow!(account)
|
||||
follower_2.follow!(account)
|
||||
follower_3.follow!(account)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:signed_request_account).and_return(remote_account)
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
context 'without signature' do
|
||||
let(:remote_account) { nil }
|
||||
|
||||
before do
|
||||
get :show, params: { account_username: account.username }
|
||||
end
|
||||
|
||||
it 'returns http not authorized' do
|
||||
expect(response).to have_http_status(401)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with signature from example.com' do
|
||||
let(:remote_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/instance') }
|
||||
|
||||
subject(:response) { get :show, params: { account_username: account.username } }
|
||||
subject(:body) { body_as_json }
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'returns application/activity+json' do
|
||||
expect(response.content_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it 'returns orderedItems with followers from example.com' do
|
||||
expect(body[:orderedItems]).to be_an Array
|
||||
expect(body[:orderedItems].sort).to eq [follower_1.uri, follower_2.uri]
|
||||
end
|
||||
|
||||
it 'returns private Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
|
||||
end
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,6 +20,83 @@ RSpec.describe ActivityPub::InboxesController, type: :controller do
|
|||
it 'returns http accepted' do
|
||||
expect(response).to have_http_status(202)
|
||||
end
|
||||
|
||||
context 'for a specific account' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
subject(:response) { post :create, params: { account_username: account.username }, body: '{}' }
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
end
|
||||
|
||||
it 'returns http accepted' do
|
||||
expect(response).to have_http_status(202)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Collection-Synchronization header' do
|
||||
let(:remote_account) { Fabricate(:account, followers_url: 'https://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor', protocol: :activitypub) }
|
||||
let(:synchronization_collection) { remote_account.followers_url }
|
||||
let(:synchronization_url) { 'https://example.com/followers-for-domain' }
|
||||
let(:synchronization_hash) { 'somehash' }
|
||||
let(:synchronization_header) { "collectionId=\"#{synchronization_collection}\", digest=\"#{synchronization_hash}\", url=\"#{synchronization_url}\"" }
|
||||
|
||||
before do
|
||||
allow(ActivityPub::FollowersSynchronizationWorker).to receive(:perform_async).and_return(nil)
|
||||
allow_any_instance_of(Account).to receive(:local_followers_hash).and_return('somehash')
|
||||
|
||||
request.headers['Collection-Synchronization'] = synchronization_header
|
||||
post :create, body: '{}'
|
||||
end
|
||||
|
||||
context 'with mismatching target collection' do
|
||||
let(:synchronization_collection) { 'https://example.com/followers2' }
|
||||
|
||||
it 'does not start a synchronization job' do
|
||||
expect(ActivityPub::FollowersSynchronizationWorker).not_to have_received(:perform_async)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with mismatching domain in partial collection attribute' do
|
||||
let(:synchronization_url) { 'https://example.org/followers' }
|
||||
|
||||
it 'does not start a synchronization job' do
|
||||
expect(ActivityPub::FollowersSynchronizationWorker).not_to have_received(:perform_async)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with matching digest' do
|
||||
it 'does not start a synchronization job' do
|
||||
expect(ActivityPub::FollowersSynchronizationWorker).not_to have_received(:perform_async)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with mismatching digest' do
|
||||
let(:synchronization_hash) { 'wronghash' }
|
||||
|
||||
it 'starts a synchronization job' do
|
||||
expect(ActivityPub::FollowersSynchronizationWorker).to have_received(:perform_async)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns http accepted' do
|
||||
expect(response).to have_http_status(202)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without signature' do
|
||||
|
|
|
@ -3,6 +3,22 @@ require 'rails_helper'
|
|||
RSpec.describe ActivityPub::OutboxesController, type: :controller do
|
||||
let!(:account) { Fabricate(:account) }
|
||||
|
||||
shared_examples 'cachable response' do
|
||||
it 'does not set cookies' do
|
||||
expect(response.cookies).to be_empty
|
||||
expect(response.headers['Set-Cookies']).to be nil
|
||||
end
|
||||
|
||||
it 'does not set sessions' do
|
||||
response
|
||||
expect(session).to be_empty
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Fabricate(:status, account: account, visibility: :public)
|
||||
Fabricate(:status, account: account, visibility: :unlisted)
|
||||
|
@ -19,9 +35,8 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
|
|||
context 'without signature' do
|
||||
let(:remote_account) { nil }
|
||||
|
||||
before do
|
||||
get :show, params: { account_username: account.username, page: page }
|
||||
end
|
||||
subject(:response) { get :show, params: { account_username: account.username, page: page } }
|
||||
subject(:body) { body_as_json }
|
||||
|
||||
context 'with page not requested' do
|
||||
let(:page) { nil }
|
||||
|
@ -35,12 +50,30 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
|
|||
end
|
||||
|
||||
it 'returns totalItems' do
|
||||
json = body_as_json
|
||||
expect(json[:totalItems]).to eq 4
|
||||
expect(body[:totalItems]).to eq 4
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
it_behaves_like 'cachable response'
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -56,14 +89,32 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
|
|||
end
|
||||
|
||||
it 'returns orderedItems with public or unlisted statuses' do
|
||||
json = body_as_json
|
||||
expect(json[:orderedItems]).to be_an Array
|
||||
expect(json[:orderedItems].size).to eq 2
|
||||
expect(json[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
|
||||
expect(body[:orderedItems]).to be_an Array
|
||||
expect(body[:orderedItems].size).to eq 2
|
||||
expect(body[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
it_behaves_like 'cachable response'
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,22 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
|
|||
let(:remote_reply_id) { nil }
|
||||
let(:remote_account) { nil }
|
||||
|
||||
shared_examples 'cachable response' do
|
||||
it 'does not set cookies' do
|
||||
expect(response.cookies).to be_empty
|
||||
expect(response.headers['Set-Cookies']).to be nil
|
||||
end
|
||||
|
||||
it 'does not set sessions' do
|
||||
response
|
||||
expect(session).to be_empty
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:signed_request_account).and_return(remote_account)
|
||||
|
||||
|
@ -21,8 +37,32 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
|
|||
|
||||
describe 'GET #index' do
|
||||
context 'with no signature' do
|
||||
before do
|
||||
get :index, params: { account_username: status.account.username, status_id: status.id }
|
||||
subject(:response) { get :index, params: { account_username: status.account.username, status_id: status.id } }
|
||||
subject(:body) { body_as_json }
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
let(:parent_visibility) { :public }
|
||||
|
||||
before do
|
||||
status.account.suspend!
|
||||
status.account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
let(:parent_visibility) { :public }
|
||||
|
||||
before do
|
||||
status.account.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when status is public' do
|
||||
|
@ -36,17 +76,13 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
|
|||
expect(response.content_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
it_behaves_like 'cachable response'
|
||||
|
||||
it 'returns items with account\'s own replies' do
|
||||
json = body_as_json
|
||||
|
||||
expect(json[:first]).to be_a Hash
|
||||
expect(json[:first][:items]).to be_an Array
|
||||
expect(json[:first][:items].size).to eq 1
|
||||
expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
|
||||
expect(body[:first]).to be_a Hash
|
||||
expect(body[:first][:items]).to be_an Array
|
||||
expect(body[:first][:items].size).to eq 1
|
||||
expect(body[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -87,9 +123,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
|
|||
expect(response.content_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
it_behaves_like 'cachable response'
|
||||
|
||||
context 'without only_other_accounts' do
|
||||
it 'returns items with account\'s own replies' do
|
||||
|
|
|
@ -9,10 +9,10 @@ RSpec.describe Admin::InstancesController, type: :controller do
|
|||
|
||||
describe 'GET #index' do
|
||||
around do |example|
|
||||
default_per_page = Account.default_per_page
|
||||
Account.paginates_per 1
|
||||
default_per_page = Instance.default_per_page
|
||||
Instance.paginates_per 1
|
||||
example.run
|
||||
Account.paginates_per default_per_page
|
||||
Instance.paginates_per default_per_page
|
||||
end
|
||||
|
||||
it 'renders instances' do
|
||||
|
|
|
@ -1,20 +1,51 @@
|
|||
require 'rails_helper'
|
||||
require 'webauthn/fake_client'
|
||||
|
||||
describe Admin::TwoFactorAuthenticationsController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, otp_required_for_login: true) }
|
||||
let(:user) { Fabricate(:user) }
|
||||
before do
|
||||
sign_in Fabricate(:user, admin: true), scope: :user
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
it 'redirects to admin accounts page' do
|
||||
delete :destroy, params: { user_id: user.id }
|
||||
context 'when user has OTP enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
user.reload
|
||||
expect(user.otp_required_for_login).to eq false
|
||||
expect(response).to redirect_to(admin_accounts_path)
|
||||
it 'redirects to admin accounts page' do
|
||||
delete :destroy, params: { user_id: user.id }
|
||||
|
||||
user.reload
|
||||
expect(user.otp_enabled?).to eq false
|
||||
expect(response).to redirect_to(admin_accounts_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has OTP and WebAuthn enabled' do
|
||||
let(:fake_client) { WebAuthn::FakeClient.new('http://test.host') }
|
||||
|
||||
before do
|
||||
user.update(otp_required_for_login: true, webauthn_id: WebAuthn.generate_user_id)
|
||||
|
||||
public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
|
||||
Fabricate(:webauthn_credential,
|
||||
user_id: user.id,
|
||||
external_id: public_key_credential.id,
|
||||
public_key: public_key_credential.public_key,
|
||||
nickname: 'Security Key')
|
||||
end
|
||||
|
||||
it 'redirects to admin accounts page' do
|
||||
delete :destroy, params: { user_id: user.id }
|
||||
|
||||
user.reload
|
||||
expect(user.otp_enabled?).to eq false
|
||||
expect(user.webauthn_enabled?).to eq false
|
||||
expect(response).to redirect_to(admin_accounts_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -71,50 +71,80 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
|
|||
let(:scopes) { 'write:follows' }
|
||||
let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', locked: locked)).account }
|
||||
|
||||
before do
|
||||
post :follow, params: { id: other_account.id }
|
||||
end
|
||||
|
||||
context 'with unlocked account' do
|
||||
let(:locked) { false }
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
context do
|
||||
before do
|
||||
post :follow, params: { id: other_account.id }
|
||||
end
|
||||
|
||||
it 'returns JSON with following=true and requested=false' do
|
||||
context 'with unlocked account' do
|
||||
let(:locked) { false }
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'returns JSON with following=true and requested=false' do
|
||||
json = body_as_json
|
||||
|
||||
expect(json[:following]).to be true
|
||||
expect(json[:requested]).to be false
|
||||
end
|
||||
|
||||
it 'creates a following relation between user and target user' do
|
||||
expect(user.account.following?(other_account)).to be true
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for wrong scope', 'read:accounts'
|
||||
end
|
||||
|
||||
context 'with locked account' do
|
||||
let(:locked) { true }
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'returns JSON with following=false and requested=true' do
|
||||
json = body_as_json
|
||||
|
||||
expect(json[:following]).to be false
|
||||
expect(json[:requested]).to be true
|
||||
end
|
||||
|
||||
it 'creates a follow request relation between user and target user' do
|
||||
expect(user.account.requested?(other_account)).to be true
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for wrong scope', 'read:accounts'
|
||||
end
|
||||
end
|
||||
|
||||
context 'modifying follow options' do
|
||||
let(:locked) { false }
|
||||
|
||||
before do
|
||||
user.account.follow!(other_account, reblogs: false, notify: false)
|
||||
end
|
||||
|
||||
it 'changes reblogs option' do
|
||||
post :follow, params: { id: other_account.id, reblogs: true }
|
||||
|
||||
json = body_as_json
|
||||
|
||||
expect(json[:following]).to be true
|
||||
expect(json[:requested]).to be false
|
||||
expect(json[:showing_reblogs]).to be true
|
||||
expect(json[:notifying]).to be false
|
||||
end
|
||||
|
||||
it 'creates a following relation between user and target user' do
|
||||
expect(user.account.following?(other_account)).to be true
|
||||
end
|
||||
it 'changes notify option' do
|
||||
post :follow, params: { id: other_account.id, notify: true }
|
||||
|
||||
it_behaves_like 'forbidden for wrong scope', 'read:accounts'
|
||||
end
|
||||
|
||||
context 'with locked account' do
|
||||
let(:locked) { true }
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'returns JSON with following=false and requested=true' do
|
||||
json = body_as_json
|
||||
|
||||
expect(json[:following]).to be false
|
||||
expect(json[:requested]).to be true
|
||||
expect(json[:following]).to be true
|
||||
expect(json[:showing_reblogs]).to be false
|
||||
expect(json[:notifying]).to be true
|
||||
end
|
||||
|
||||
it 'creates a follow request relation between user and target user' do
|
||||
expect(user.account.requested?(other_account)).to be true
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for wrong scope', 'read:accounts'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
|
|||
|
||||
describe 'POST #unsuspend' do
|
||||
before do
|
||||
account.touch(:suspended_at)
|
||||
account.suspend!
|
||||
post :unsuspend, params: { id: account.id }
|
||||
end
|
||||
|
||||
|
@ -127,6 +127,24 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'POST #unsensitive' do
|
||||
before do
|
||||
account.touch(:sensitized_at)
|
||||
post :unsensitive, params: { id: account.id }
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
|
||||
it_behaves_like 'forbidden for wrong role', 'user'
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'unsensitives account' do
|
||||
expect(account.reload.sensitized?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #unsilence' do
|
||||
before do
|
||||
account.touch(:silenced_at)
|
||||
|
|
|
@ -72,6 +72,31 @@ describe Api::V1::Statuses::BookmarksController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with public status when blocked by its author' do
|
||||
let(:status) { Fabricate(:status) }
|
||||
|
||||
before do
|
||||
Bookmark.find_or_create_by!(account: user.account, status: status)
|
||||
status.account.block!(user.account)
|
||||
post :destroy, params: { status_id: status.id }
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'updates the bookmarked attribute' do
|
||||
expect(user.account.bookmarked?(status)).to be false
|
||||
end
|
||||
|
||||
it 'returns json with updated attributes' do
|
||||
hash_body = body_as_json
|
||||
|
||||
expect(hash_body[:id]).to eq status.id.to_s
|
||||
expect(hash_body[:bookmarked]).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'with private status that was not bookmarked' do
|
||||
let(:status) { Fabricate(:status, visibility: :private) }
|
||||
|
||||
|
|
|
@ -82,6 +82,31 @@ describe Api::V1::Statuses::FavouritesController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with public status when blocked by its author' do
|
||||
let(:status) { Fabricate(:status) }
|
||||
|
||||
before do
|
||||
FavouriteService.new.call(user.account, status)
|
||||
status.account.block!(user.account)
|
||||
post :destroy, params: { status_id: status.id }
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'updates the favourite attribute' do
|
||||
expect(user.account.favourited?(status)).to be false
|
||||
end
|
||||
|
||||
it 'returns json with updated attributes' do
|
||||
hash_body = body_as_json
|
||||
|
||||
expect(hash_body[:id]).to eq status.id.to_s
|
||||
expect(hash_body[:favourited]).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'with private status that was not favourited' do
|
||||
let(:status) { Fabricate(:status, visibility: :private) }
|
||||
|
||||
|
|
|
@ -82,6 +82,10 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
|
|||
describe 'POST #create' do
|
||||
let(:accept_language) { Rails.application.config.i18n.available_locales.sample.to_s }
|
||||
|
||||
before do
|
||||
session[:registration_form_time] = 5.seconds.ago
|
||||
end
|
||||
|
||||
around do |example|
|
||||
current_locale = I18n.locale
|
||||
example.run
|
||||
|
@ -191,17 +195,21 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
context 'approval-based registrations with valid invite' do
|
||||
context 'approval-based registrations with valid invite and required invite text' do
|
||||
around do |example|
|
||||
registrations_mode = Setting.registrations_mode
|
||||
require_invite_text = Setting.require_invite_text
|
||||
example.run
|
||||
Setting.require_invite_text = require_invite_text
|
||||
Setting.registrations_mode = registrations_mode
|
||||
end
|
||||
|
||||
subject do
|
||||
inviter = Fabricate(:user, confirmed_at: 2.days.ago)
|
||||
Setting.registrations_mode = 'approved'
|
||||
Setting.require_invite_text = true
|
||||
request.headers["Accept-Language"] = accept_language
|
||||
invite = Fabricate(:invite, max_uses: nil, expires_at: 1.hour.from_now)
|
||||
invite = Fabricate(:invite, user: inviter, max_uses: nil, expires_at: 1.hour.from_now)
|
||||
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code, agreement: 'true' } }
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
require 'webauthn/fake_client'
|
||||
|
||||
RSpec.describe Auth::SessionsController, type: :controller do
|
||||
render_views
|
||||
|
@ -183,90 +184,170 @@ RSpec.describe Auth::SessionsController, type: :controller do
|
|||
end
|
||||
|
||||
context 'using two-factor authentication' do
|
||||
let!(:user) do
|
||||
Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
|
||||
end
|
||||
|
||||
let!(:recovery_codes) do
|
||||
codes = user.generate_otp_backup_codes!
|
||||
user.save
|
||||
return codes
|
||||
end
|
||||
|
||||
context 'using email and password' do
|
||||
before do
|
||||
post :create, params: { user: { email: user.email, password: user.password } }
|
||||
context 'with OTP enabled as second factor' do
|
||||
let!(:user) do
|
||||
Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
|
||||
end
|
||||
|
||||
it 'renders two factor authentication page' do
|
||||
expect(controller).to render_template("two_factor")
|
||||
let!(:recovery_codes) do
|
||||
codes = user.generate_otp_backup_codes!
|
||||
user.save
|
||||
return codes
|
||||
end
|
||||
|
||||
context 'using email and password' do
|
||||
before do
|
||||
post :create, params: { user: { email: user.email, password: user.password } }
|
||||
end
|
||||
|
||||
it 'renders two factor authentication page' do
|
||||
expect(controller).to render_template("two_factor")
|
||||
expect(controller).to render_template(partial: "_otp_authentication_form")
|
||||
end
|
||||
end
|
||||
|
||||
context 'using upcase email and password' do
|
||||
before do
|
||||
post :create, params: { user: { email: user.email.upcase, password: user.password } }
|
||||
end
|
||||
|
||||
it 'renders two factor authentication page' do
|
||||
expect(controller).to render_template("two_factor")
|
||||
expect(controller).to render_template(partial: "_otp_authentication_form")
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a valid OTP' do
|
||||
before do
|
||||
post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||
end
|
||||
|
||||
it 'redirects to home' do
|
||||
expect(response).to redirect_to(root_path)
|
||||
end
|
||||
|
||||
it 'logs the user in' do
|
||||
expect(controller.current_user).to eq user
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the server has an decryption error' do
|
||||
before do
|
||||
allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_raise(OpenSSL::Cipher::CipherError)
|
||||
post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||
end
|
||||
|
||||
it 'shows a login error' do
|
||||
expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
|
||||
end
|
||||
|
||||
it "doesn't log the user in" do
|
||||
expect(controller.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a valid recovery code' do
|
||||
before do
|
||||
post :create, params: { user: { otp_attempt: recovery_codes.first } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||
end
|
||||
|
||||
it 'redirects to home' do
|
||||
expect(response).to redirect_to(root_path)
|
||||
end
|
||||
|
||||
it 'logs the user in' do
|
||||
expect(controller.current_user).to eq user
|
||||
end
|
||||
end
|
||||
|
||||
context 'using an invalid OTP' do
|
||||
before do
|
||||
post :create, params: { user: { otp_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||
end
|
||||
|
||||
it 'shows a login error' do
|
||||
expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
|
||||
end
|
||||
|
||||
it "doesn't log the user in" do
|
||||
expect(controller.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'using upcase email and password' do
|
||||
before do
|
||||
post :create, params: { user: { email: user.email.upcase, password: user.password } }
|
||||
context 'with WebAuthn and OTP enabled as second factor' do
|
||||
let!(:user) do
|
||||
Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
|
||||
end
|
||||
|
||||
it 'renders two factor authentication page' do
|
||||
expect(controller).to render_template("two_factor")
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a valid OTP' do
|
||||
before do
|
||||
post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id }
|
||||
let!(:recovery_codes) do
|
||||
codes = user.generate_otp_backup_codes!
|
||||
user.save
|
||||
return codes
|
||||
end
|
||||
|
||||
it 'redirects to home' do
|
||||
expect(response).to redirect_to(root_path)
|
||||
let!(:webauthn_credential) do
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
|
||||
user.webauthn_credentials.create(
|
||||
nickname: 'SecurityKeyNickname',
|
||||
external_id: public_key_credential.id,
|
||||
public_key: public_key_credential.public_key,
|
||||
sign_count: '1000'
|
||||
)
|
||||
user.webauthn_credentials.take
|
||||
end
|
||||
|
||||
it 'logs the user in' do
|
||||
expect(controller.current_user).to eq user
|
||||
end
|
||||
end
|
||||
let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http' }://#{Rails.configuration.x.web_domain}" }
|
||||
|
||||
context 'when the server has an decryption error' do
|
||||
before do
|
||||
allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_raise(OpenSSL::Cipher::CipherError)
|
||||
post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id }
|
||||
let(:fake_client) { WebAuthn::FakeClient.new(domain) }
|
||||
|
||||
let(:challenge) { WebAuthn::Credential.options_for_get.challenge }
|
||||
|
||||
let(:sign_count) { 1234 }
|
||||
|
||||
let(:fake_credential) { fake_client.get(challenge: challenge, sign_count: sign_count) }
|
||||
|
||||
context 'using email and password' do
|
||||
before do
|
||||
post :create, params: { user: { email: user.email, password: user.password } }
|
||||
end
|
||||
|
||||
it 'renders webauthn authentication page' do
|
||||
expect(controller).to render_template("two_factor")
|
||||
expect(controller).to render_template(partial: "_webauthn_form")
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows a login error' do
|
||||
expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
|
||||
context 'using upcase email and password' do
|
||||
before do
|
||||
post :create, params: { user: { email: user.email.upcase, password: user.password } }
|
||||
end
|
||||
|
||||
it 'renders webauthn authentication page' do
|
||||
expect(controller).to render_template("two_factor")
|
||||
expect(controller).to render_template(partial: "_webauthn_form")
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't log the user in" do
|
||||
expect(controller.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
context 'using a valid webauthn credential' do
|
||||
before do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
context 'using a valid recovery code' do
|
||||
before do
|
||||
post :create, params: { user: { otp_attempt: recovery_codes.first } }, session: { attempt_user_id: user.id }
|
||||
end
|
||||
post :create, params: { user: { credential: fake_credential } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||
end
|
||||
|
||||
it 'redirects to home' do
|
||||
expect(response).to redirect_to(root_path)
|
||||
end
|
||||
it 'instructs the browser to redirect to home' do
|
||||
expect(body_as_json[:redirect_path]).to eq(root_path)
|
||||
end
|
||||
|
||||
it 'logs the user in' do
|
||||
expect(controller.current_user).to eq user
|
||||
end
|
||||
end
|
||||
it 'logs the user in' do
|
||||
expect(controller.current_user).to eq user
|
||||
end
|
||||
|
||||
context 'using an invalid OTP' do
|
||||
before do
|
||||
post :create, params: { user: { otp_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id }
|
||||
end
|
||||
|
||||
it 'shows a login error' do
|
||||
expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
|
||||
end
|
||||
|
||||
it "doesn't log the user in" do
|
||||
expect(controller.current_user).to be_nil
|
||||
it 'updates the sign count' do
|
||||
expect(webauthn_credential.reload.sign_count).to eq(sign_count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -302,7 +383,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
|
|||
context 'using a valid sign in token' do
|
||||
before do
|
||||
user.generate_sign_in_token && user.save
|
||||
post :create, params: { user: { sign_in_token_attempt: user.sign_in_token } }, session: { attempt_user_id: user.id }
|
||||
post :create, params: { user: { sign_in_token_attempt: user.sign_in_token } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||
end
|
||||
|
||||
it 'redirects to home' do
|
||||
|
@ -316,7 +397,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
|
|||
|
||||
context 'using an invalid sign in token' do
|
||||
before do
|
||||
post :create, params: { user: { sign_in_token_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id }
|
||||
post :create, params: { user: { sign_in_token_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||
end
|
||||
|
||||
it 'shows a login error' do
|
||||
|
|
|
@ -5,6 +5,7 @@ require 'rails_helper'
|
|||
describe ApplicationController, type: :controller do
|
||||
controller do
|
||||
include ExportControllerConcern
|
||||
|
||||
def index
|
||||
send_export_file
|
||||
end
|
||||
|
|
|
@ -14,6 +14,27 @@ describe FollowerAccountsController do
|
|||
context 'when format is html' do
|
||||
subject(:response) { get :index, params: { account_username: alice.username, format: :html } }
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
alice.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
it 'assigns follows' do
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
|
@ -48,6 +69,27 @@ describe FollowerAccountsController do
|
|||
expect(body['totalItems']).to eq 2
|
||||
expect(body['partOf']).to be_present
|
||||
end
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
alice.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without page' do
|
||||
|
@ -58,6 +100,27 @@ describe FollowerAccountsController do
|
|||
expect(body['totalItems']).to eq 2
|
||||
expect(body['partOf']).to be_blank
|
||||
end
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
alice.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,27 @@ describe FollowingAccountsController do
|
|||
context 'when format is html' do
|
||||
subject(:response) { get :index, params: { account_username: alice.username, format: :html } }
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
alice.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
it 'assigns follows' do
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
|
@ -48,6 +69,27 @@ describe FollowingAccountsController do
|
|||
expect(body['totalItems']).to eq 2
|
||||
expect(body['partOf']).to be_present
|
||||
end
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
alice.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without page' do
|
||||
|
@ -58,6 +100,27 @@ describe FollowingAccountsController do
|
|||
expect(body['totalItems']).to eq 2
|
||||
expect(body['partOf']).to be_blank
|
||||
end
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
alice.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
alice.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,8 +43,7 @@ describe RemoteFollowController do
|
|||
end
|
||||
|
||||
it 'renders new when template is nil' do
|
||||
link_with_nil_template = double(template: nil)
|
||||
resource_with_link = double(link: link_with_nil_template)
|
||||
resource_with_link = double(link: nil)
|
||||
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link)
|
||||
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
||||
|
||||
|
@ -55,8 +54,7 @@ describe RemoteFollowController do
|
|||
|
||||
context 'when webfinger values are good' do
|
||||
before do
|
||||
link_with_template = double(template: 'http://example.com/follow_me?acct={uri}')
|
||||
resource_with_link = double(link: link_with_template)
|
||||
resource_with_link = double(link: 'http://example.com/follow_me?acct={uri}')
|
||||
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link)
|
||||
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
||||
end
|
||||
|
@ -78,8 +76,8 @@ describe RemoteFollowController do
|
|||
expect(response).to render_template(:new)
|
||||
end
|
||||
|
||||
it 'renders new with error when goldfinger fails' do
|
||||
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_raise(Goldfinger::Error)
|
||||
it 'renders new with error when webfinger fails' do
|
||||
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_raise(Webfinger::Error)
|
||||
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
||||
|
||||
expect(response).to render_template(:new)
|
||||
|
@ -96,21 +94,42 @@ describe RemoteFollowController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'with a suspended account' do
|
||||
context 'with a permanently suspended account' do
|
||||
before do
|
||||
@account = Fabricate(:account, suspended: true)
|
||||
@account = Fabricate(:account)
|
||||
@account.suspend!
|
||||
@account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns 410 gone on GET to #new' do
|
||||
it 'returns http gone on GET to #new' do
|
||||
get :new, params: { account_username: @account.to_param }
|
||||
|
||||
expect(response).to have_http_status(:gone)
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
|
||||
it 'returns 410 gone on POST to #create' do
|
||||
it 'returns http gone on POST to #create' do
|
||||
post :create, params: { account_username: @account.to_param }
|
||||
|
||||
expect(response).to have_http_status(:gone)
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a temporarily suspended account' do
|
||||
before do
|
||||
@account = Fabricate(:account)
|
||||
@account.suspend!
|
||||
end
|
||||
|
||||
it 'returns http forbidden on GET to #new' do
|
||||
get :new, params: { account_username: @account.to_param }
|
||||
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
|
||||
it 'returns http forbidden on POST to #create' do
|
||||
post :create, params: { account_username: @account.to_param }
|
||||
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -77,6 +77,20 @@ describe Settings::DeletesController do
|
|||
expect(response).to redirect_to settings_delete_path
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account deletions are disabled' do
|
||||
around do |example|
|
||||
open_deletion = Setting.open_deletion
|
||||
example.run
|
||||
Setting.open_deletion = open_deletion
|
||||
end
|
||||
|
||||
it 'redirects' do
|
||||
Setting.open_deletion = false
|
||||
delete :destroy
|
||||
expect(response).to redirect_to root_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
|
@ -85,19 +99,5 @@ describe Settings::DeletesController do
|
|||
expect(response).to redirect_to '/auth/sign_in'
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
around do |example|
|
||||
open_deletion = Setting.open_deletion
|
||||
example.run
|
||||
Setting.open_deletion = open_deletion
|
||||
end
|
||||
|
||||
it 'redirects' do
|
||||
Setting.open_deletion = false
|
||||
delete :destroy
|
||||
expect(response).to redirect_to root_path
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe Settings::Exports::BookmarksController do
|
||||
render_views
|
||||
|
||||
describe 'GET #index' do
|
||||
it 'returns a csv of the bookmarked toots' do
|
||||
user = Fabricate(:user)
|
||||
user.account.bookmarks.create!(status: Fabricate(:status, uri: 'https://foo.bar/statuses/1312'))
|
||||
|
||||
sign_in user, scope: :user
|
||||
get :index, format: :csv
|
||||
|
||||
expect(response.body).to eq "https://foo.bar/statuses/1312\n"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,8 +5,6 @@ require 'rails_helper'
|
|||
describe Settings::TwoFactorAuthentication::ConfirmationsController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user, email: 'local-part@domain', otp_secret: 'thisisasecretforthespecofnewview') }
|
||||
let(:user_without_otp_secret) { Fabricate(:user, email: 'local-part@domain') }
|
||||
|
||||
shared_examples 'renders :new' do
|
||||
it 'renders the new view' do
|
||||
|
@ -20,87 +18,101 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
context 'when signed in' do
|
||||
subject do
|
||||
sign_in user, scope: :user
|
||||
get :new, session: { challenge_passed_at: Time.now.utc }
|
||||
end
|
||||
[true, false].each do |with_otp_secret|
|
||||
let(:user) { Fabricate(:user, email: 'local-part@domain', otp_secret: with_otp_secret ? 'oldotpsecret' : nil) }
|
||||
|
||||
include_examples 'renders :new'
|
||||
end
|
||||
|
||||
it 'redirects if not signed in' do
|
||||
get :new
|
||||
expect(response).to redirect_to('/auth/sign_in')
|
||||
end
|
||||
|
||||
it 'redirects if user do not have otp_secret' do
|
||||
sign_in user_without_otp_secret, scope: :user
|
||||
get :new, session: { challenge_passed_at: Time.now.utc }
|
||||
expect(response).to redirect_to('/settings/two_factor_authentication')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
describe 'when form_two_factor_confirmation parameter is not provided' do
|
||||
it 'raises ActionController::ParameterMissing' do
|
||||
post :create, params: {}, session: { challenge_passed_at: Time.now.utc }
|
||||
expect(response).to have_http_status(400)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when creation succeeds' do
|
||||
it 'renders page with success' do
|
||||
otp_backup_codes = user.generate_otp_backup_codes!
|
||||
expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value|
|
||||
expect(value).to eq user
|
||||
otp_backup_codes
|
||||
end
|
||||
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, arg|
|
||||
expect(value).to eq user
|
||||
expect(arg).to eq '123456'
|
||||
true
|
||||
end
|
||||
|
||||
post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }, session: { challenge_passed_at: Time.now.utc }
|
||||
|
||||
expect(assigns(:recovery_codes)).to eq otp_backup_codes
|
||||
expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled'
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when creation fails' do
|
||||
describe 'GET #new' do
|
||||
context 'when signed in and a new otp secret has been setted in the session' do
|
||||
subject do
|
||||
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, arg|
|
||||
expect(value).to eq user
|
||||
expect(arg).to eq '123456'
|
||||
false
|
||||
end
|
||||
|
||||
post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }, session: { challenge_passed_at: Time.now.utc }
|
||||
end
|
||||
|
||||
it 'renders the new view' do
|
||||
subject
|
||||
expect(response.body).to include 'The entered code was invalid! Are server time and device time correct?'
|
||||
sign_in user, scope: :user
|
||||
get :new, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
|
||||
end
|
||||
|
||||
include_examples 'renders :new'
|
||||
end
|
||||
|
||||
it 'redirects if not signed in' do
|
||||
get :new
|
||||
expect(response).to redirect_to('/auth/sign_in')
|
||||
end
|
||||
|
||||
it 'redirects if a new otp_secret has not been setted in the session' do
|
||||
sign_in user, scope: :user
|
||||
get :new, session: { challenge_passed_at: Time.now.utc }
|
||||
expect(response).to redirect_to('/settings/otp_authentication')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects if not signed in' do
|
||||
post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }
|
||||
expect(response).to redirect_to('/auth/sign_in')
|
||||
describe 'POST #create' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
describe 'when form_two_factor_confirmation parameter is not provided' do
|
||||
it 'raises ActionController::ParameterMissing' do
|
||||
post :create, params: {}, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
|
||||
expect(response).to have_http_status(400)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when creation succeeds' do
|
||||
it 'renders page with success' do
|
||||
otp_backup_codes = user.generate_otp_backup_codes!
|
||||
expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value|
|
||||
expect(value).to eq user
|
||||
otp_backup_codes
|
||||
end
|
||||
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, code, options|
|
||||
expect(value).to eq user
|
||||
expect(code).to eq '123456'
|
||||
expect(options).to eq({ otp_secret: 'thisisasecretforthespecofnewview' })
|
||||
true
|
||||
end
|
||||
|
||||
expect do
|
||||
post :create,
|
||||
params: { form_two_factor_confirmation: { otp_attempt: '123456' } },
|
||||
session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
|
||||
end.to change { user.reload.otp_secret }.to 'thisisasecretforthespecofnewview'
|
||||
|
||||
expect(assigns(:recovery_codes)).to eq otp_backup_codes
|
||||
expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled'
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when creation fails' do
|
||||
subject do
|
||||
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, code, options|
|
||||
expect(value).to eq user
|
||||
expect(code).to eq '123456'
|
||||
expect(options).to eq({ otp_secret: 'thisisasecretforthespecofnewview' })
|
||||
false
|
||||
end
|
||||
|
||||
expect do
|
||||
post :create,
|
||||
params: { form_two_factor_confirmation: { otp_attempt: '123456' } },
|
||||
session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
|
||||
end.to not_change { user.reload.otp_secret }
|
||||
end
|
||||
|
||||
it 'renders the new view' do
|
||||
subject
|
||||
expect(response.body).to include 'The entered code was invalid! Are server time and device time correct?'
|
||||
end
|
||||
|
||||
include_examples 'renders :new'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects if not signed in' do
|
||||
post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }
|
||||
expect(response).to redirect_to('/auth/sign_in')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Settings::TwoFactorAuthentication::OtpAuthenticationController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
describe 'GET #show' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
describe 'when user has OTP enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
it 'redirects to two factor authentciation methods list page' do
|
||||
get :show
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when user does not have OTP enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
get :show
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects' do
|
||||
get :show
|
||||
|
||||
expect(response).to redirect_to new_user_session_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
describe 'when user has OTP enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
describe 'when creation succeeds' do
|
||||
it 'redirects to code confirmation page without updating user secret and setting otp secret in the session' do
|
||||
expect do
|
||||
post :create, session: { challenge_passed_at: Time.now.utc }
|
||||
end.to not_change { user.reload.otp_secret }
|
||||
.and change { session[:new_otp_secret] }
|
||||
|
||||
expect(response).to redirect_to(new_settings_two_factor_authentication_confirmation_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when user does not have OTP enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
|
||||
describe 'when creation succeeds' do
|
||||
it 'redirects to code confirmation page without updating user secret and setting otp secret in the session' do
|
||||
expect do
|
||||
post :create, session: { challenge_passed_at: Time.now.utc }
|
||||
end.to not_change { user.reload.otp_secret }
|
||||
.and change { session[:new_otp_secret] }
|
||||
|
||||
expect(response).to redirect_to(new_settings_two_factor_authentication_confirmation_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects to login' do
|
||||
get :show
|
||||
|
||||
expect(response).to redirect_to new_user_session_path
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,374 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
require 'webauthn/fake_client'
|
||||
|
||||
describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http' }://#{Rails.configuration.x.web_domain}" }
|
||||
let(:fake_client) { WebAuthn::FakeClient.new(domain) }
|
||||
|
||||
def add_webauthn_credential(user)
|
||||
Fabricate(:webauthn_credential, user_id: user.id, nickname: 'USB Key')
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
context 'when user has otp enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
get :new
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have otp enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
|
||||
it 'requires otp enabled first' do
|
||||
get :new
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
context 'when user has otp enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
context 'when user has webauthn enabled' do
|
||||
before do
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
add_webauthn_credential(user)
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
get :index
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not has webauthn enabled' do
|
||||
it 'redirects to 2FA methods list page' do
|
||||
get :index
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have otp enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
|
||||
it 'requires otp enabled first' do
|
||||
get :index
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects to login' do
|
||||
delete :index
|
||||
|
||||
expect(response).to redirect_to new_user_session_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /options #options' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
context 'when user has otp enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
context 'when user has webauthn enabled' do
|
||||
before do
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
add_webauthn_credential(user)
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
get :options
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'stores the challenge on the session' do
|
||||
get :options
|
||||
|
||||
expect(@controller.session[:webauthn_challenge]).to be_present
|
||||
end
|
||||
|
||||
it 'does not change webauthn_id' do
|
||||
expect { get :options }.to_not change { user.webauthn_id }
|
||||
end
|
||||
|
||||
it "includes existing credentials in list of excluded credentials" do
|
||||
get :options
|
||||
|
||||
excluded_credentials_ids = JSON.parse(response.body)['excludeCredentials'].map { |credential| credential['id'] }
|
||||
expect(excluded_credentials_ids).to match_array(user.webauthn_credentials.pluck(:external_id))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have webauthn enabled' do
|
||||
it 'returns http success' do
|
||||
get :options
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'stores the challenge on the session' do
|
||||
get :options
|
||||
|
||||
expect(@controller.session[:webauthn_challenge]).to be_present
|
||||
end
|
||||
|
||||
it 'sets user webauthn_id' do
|
||||
get :options
|
||||
|
||||
expect(user.reload.webauthn_id).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has not enabled otp' do
|
||||
before do
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
|
||||
it 'requires otp enabled first' do
|
||||
get :options
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects to login' do
|
||||
get :options
|
||||
|
||||
expect(response).to redirect_to new_user_session_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
let(:nickname) { 'SecurityKeyNickname' }
|
||||
|
||||
let(:challenge) do
|
||||
WebAuthn::Credential.options_for_create(
|
||||
user: { id: user.id, name: user.account.username, display_name: user.account.display_name }
|
||||
).challenge
|
||||
end
|
||||
|
||||
let(:new_webauthn_credential) { fake_client.create(challenge: challenge) }
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
context 'when user has enabled otp' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
context 'when user has enabled webauthn' do
|
||||
before do
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
add_webauthn_credential(user)
|
||||
end
|
||||
|
||||
context 'when creation succeeds' do
|
||||
it 'returns http success' do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'adds a new credential to user credentials' do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
expect do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
end.to change { user.webauthn_credentials.count }.by(1)
|
||||
end
|
||||
|
||||
it 'does not change webauthn_id' do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
expect do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
end.to_not change { user.webauthn_id }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the nickname is already used' do
|
||||
it 'fails' do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: 'USB Key' }
|
||||
|
||||
expect(response).to have_http_status(500)
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the credential already exists' do
|
||||
before do
|
||||
user2 = Fabricate(:user)
|
||||
public_key_credential = WebAuthn::Credential.from_create(new_webauthn_credential)
|
||||
Fabricate(:webauthn_credential,
|
||||
user_id: user2.id,
|
||||
external_id: public_key_credential.id,
|
||||
public_key: public_key_credential.public_key)
|
||||
end
|
||||
|
||||
it 'fails' do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
||||
expect(response).to have_http_status(500)
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user have not enabled webauthn' do
|
||||
context 'creation succeeds' do
|
||||
it 'creates a webauthn credential' do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
expect do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
end.to change { user.webauthn_credentials.count }.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has not enabled otp' do
|
||||
before do
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
|
||||
it 'requires otp enabled first' do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects to login' do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
||||
expect(response).to redirect_to new_user_session_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
context 'when user has otp enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
context 'when user has webauthn enabled' do
|
||||
before do
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
add_webauthn_credential(user)
|
||||
end
|
||||
|
||||
context 'when deletion succeeds' do
|
||||
it 'redirects to 2FA methods list and shows flash success' do
|
||||
delete :destroy, params: { id: user.webauthn_credentials.take.id }
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:success]).to be_present
|
||||
end
|
||||
|
||||
it 'deletes the credential' do
|
||||
expect do
|
||||
delete :destroy, params: { id: user.webauthn_credentials.take.id }
|
||||
end.to change { user.webauthn_credentials.count }.by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have webauthn enabled' do
|
||||
it 'redirects to 2FA methods list and shows flash error' do
|
||||
delete :destroy, params: { id: '1' }
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have otp enabled' do
|
||||
it 'requires otp enabled first' do
|
||||
delete :destroy, params: { id: '1' }
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects to login' do
|
||||
delete :destroy, params: { id: '1' }
|
||||
|
||||
expect(response).to redirect_to new_user_session_path
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Settings::TwoFactorAuthenticationMethodsController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
describe 'GET #index' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
describe 'when user has enabled otp' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
get :index
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when user has not enabled otp' do
|
||||
before do
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
|
||||
it 'redirects to enable otp' do
|
||||
get :index
|
||||
|
||||
expect(response).to redirect_to(settings_otp_authentication_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects' do
|
||||
get :index
|
||||
|
||||
expect(response).to redirect_to '/auth/sign_in'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,125 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Settings::TwoFactorAuthenticationsController do
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
describe 'GET #show' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
describe 'when user requires otp for login already' do
|
||||
it 'returns http success' do
|
||||
user.update(otp_required_for_login: true)
|
||||
get :show
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when user does not require otp for login' do
|
||||
it 'returns http success' do
|
||||
user.update(otp_required_for_login: false)
|
||||
get :show
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects' do
|
||||
get :show
|
||||
expect(response).to redirect_to '/auth/sign_in'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
describe 'when user requires otp for login already' do
|
||||
it 'redirects to show page' do
|
||||
user.update(otp_required_for_login: true)
|
||||
post :create
|
||||
|
||||
expect(response).to redirect_to(settings_two_factor_authentication_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when creation succeeds' do
|
||||
it 'updates user secret' do
|
||||
before = user.otp_secret
|
||||
post :create, session: { challenge_passed_at: Time.now.utc }
|
||||
|
||||
expect(user.reload.otp_secret).not_to eq(before)
|
||||
expect(response).to redirect_to(new_settings_two_factor_authentication_confirmation_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects' do
|
||||
get :show
|
||||
expect(response).to redirect_to '/auth/sign_in'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #destroy' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
it 'turns off otp requirement with correct code' do
|
||||
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, arg|
|
||||
expect(value).to eq user
|
||||
expect(arg).to eq '123456'
|
||||
true
|
||||
end
|
||||
|
||||
post :destroy, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }
|
||||
|
||||
expect(response).to redirect_to(settings_two_factor_authentication_path)
|
||||
user.reload
|
||||
expect(user.otp_required_for_login).to eq(false)
|
||||
end
|
||||
|
||||
it 'does not turn off otp if code is incorrect' do
|
||||
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, arg|
|
||||
expect(value).to eq user
|
||||
expect(arg).to eq '057772'
|
||||
false
|
||||
end
|
||||
|
||||
post :destroy, params: { form_two_factor_confirmation: { otp_attempt: '057772' } }
|
||||
|
||||
user.reload
|
||||
expect(user.otp_required_for_login).to eq(true)
|
||||
end
|
||||
|
||||
it 'raises ActionController::ParameterMissing if code is missing' do
|
||||
post :destroy
|
||||
expect(response).to have_http_status(400)
|
||||
end
|
||||
end
|
||||
|
||||
it 'redirects if not signed in' do
|
||||
get :show
|
||||
expect(response).to redirect_to '/auth/sign_in'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,14 +5,30 @@ require 'rails_helper'
|
|||
describe StatusesController do
|
||||
render_views
|
||||
|
||||
shared_examples 'cachable response' do
|
||||
it 'does not set cookies' do
|
||||
expect(response.cookies).to be_empty
|
||||
expect(response.headers['Set-Cookies']).to be nil
|
||||
end
|
||||
|
||||
it 'does not set sessions' do
|
||||
expect(session).to be_empty
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:status) { Fabricate(:status, account: account) }
|
||||
|
||||
context 'when account is suspended' do
|
||||
let(:account) { Fabricate(:account, suspended: true) }
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
|
||||
get :show, params: { account_username: account.username, id: status.id }
|
||||
end
|
||||
|
||||
|
@ -21,6 +37,18 @@ describe StatusesController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
|
||||
get :show, params: { account_username: account.username, id: status.id }
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when status is a reblog' do
|
||||
let(:original_account) { Fabricate(:account, domain: 'example.com') }
|
||||
let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') }
|
||||
|
@ -80,9 +108,7 @@ describe StatusesController do
|
|||
expect(response.headers['Vary']).to eq 'Accept'
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
it_behaves_like 'cachable response'
|
||||
|
||||
it 'returns Content-Type header' do
|
||||
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
||||
|
@ -470,9 +496,7 @@ describe StatusesController do
|
|||
expect(response.headers['Vary']).to eq 'Accept'
|
||||
end
|
||||
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
it_behaves_like 'cachable response'
|
||||
|
||||
it 'returns Content-Type header' do
|
||||
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
||||
|
@ -665,10 +689,11 @@ describe StatusesController do
|
|||
let(:account) { Fabricate(:account) }
|
||||
let(:status) { Fabricate(:status, account: account) }
|
||||
|
||||
context 'when account is suspended' do
|
||||
let(:account) { Fabricate(:account, suspended: true) }
|
||||
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
|
||||
get :activity, params: { account_username: account.username, id: status.id }
|
||||
end
|
||||
|
||||
|
@ -677,6 +702,18 @@ describe StatusesController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
|
||||
get :activity, params: { account_username: account.username, id: status.id }
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when status is public' do
|
||||
pending
|
||||
end
|
||||
|
|
|
@ -12,7 +12,7 @@ describe WellKnown::HostMetaController, type: :controller do
|
|||
expect(response.body).to eq <<XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||
<Link rel="lrdd" type="application/xrd+xml" template="https://cb6e6126.ngrok.io/.well-known/webfinger?resource={uri}"/>
|
||||
<Link rel="lrdd" template="https://cb6e6126.ngrok.io/.well-known/webfinger?resource={uri}"/>
|
||||
</XRD>
|
||||
XML
|
||||
end
|
||||
|
|
|
@ -4,95 +4,134 @@ describe WellKnown::WebfingerController, type: :controller do
|
|||
render_views
|
||||
|
||||
describe 'GET #show' do
|
||||
let(:alice) do
|
||||
Fabricate(:account, username: 'alice')
|
||||
end
|
||||
|
||||
before do
|
||||
alice.private_key = <<-PEM
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQDHgPoPJlrfMZrVcuF39UbVssa8r4ObLP3dYl9Y17Mgp5K4mSYD
|
||||
R/Y2ag58tSi6ar2zM3Ze3QYsNfTq0NqN1g89eAu0MbSjWqpOsgntRPJiFuj3hai2
|
||||
X2Im8TBrkiM/UyfTRgn8q8WvMoKbXk8Lu6nqv420eyqhhLxfUoCpxuem1QIDAQAB
|
||||
AoGBAIKsOh2eM7spVI8mdgQKheEG/iEsnPkQ2R8ehfE9JzjmSbXbqghQJDaz9NU+
|
||||
G3Uu4R31QT0VbCudE9SSA/UPFl82GeQG4QLjrSE+PSjSkuslgSXelJHfAJ+ycGax
|
||||
ajtPyiQD0e4c2loagHNHPjqK9OhHx9mFnZWmoagjlZ+mQGEpAkEA8GtqfS65IaRQ
|
||||
uVhMzpp25rF1RWOwaaa+vBPkd7pGdJEQGFWkaR/a9UkU+2C4ZxGBkJDP9FApKVQI
|
||||
RANEwN3/hwJBANRuw5+es6BgBv4PD387IJvuruW2oUtYP+Lb2Z5k77J13hZTr0db
|
||||
Oo9j1UbbR0/4g+vAcsDl4JD9c/9LrGYEpcMCQBon9Yvs+2M3lziy7JhFoc3zXIjS
|
||||
Ea1M4M9hcqe78lJYPeIH3z04o/+vlcLLgQRlmSz7NESmO/QtGkEcAezhuh0CQHji
|
||||
pzO4LeO/gXslut3eGcpiYuiZquOjToecMBRwv+5AIKd367Che4uJdh6iPcyGURvh
|
||||
IewfZFFdyZqnx20ui90CQQC1W2rK5Y30wAunOtSLVA30TLK/tKrTppMC3corjKlB
|
||||
FTX8IvYBNTbpEttc1VCf/0ccnNpfb0CrFNSPWxRj7t7D
|
||||
-----END RSA PRIVATE KEY-----
|
||||
PEM
|
||||
|
||||
alice.public_key = <<-PEM
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHgPoPJlrfMZrVcuF39UbVssa8
|
||||
r4ObLP3dYl9Y17Mgp5K4mSYDR/Y2ag58tSi6ar2zM3Ze3QYsNfTq0NqN1g89eAu0
|
||||
MbSjWqpOsgntRPJiFuj3hai2X2Im8TBrkiM/UyfTRgn8q8WvMoKbXk8Lu6nqv420
|
||||
eyqhhLxfUoCpxuem1QIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
PEM
|
||||
|
||||
alice.save!
|
||||
end
|
||||
let(:alternate_domains) { [] }
|
||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||
let(:resource) { nil }
|
||||
|
||||
around(:each) do |example|
|
||||
before = Rails.configuration.x.alternate_domains
|
||||
tmp = Rails.configuration.x.alternate_domains
|
||||
Rails.configuration.x.alternate_domains = alternate_domains
|
||||
example.run
|
||||
Rails.configuration.x.alternate_domains = before
|
||||
Rails.configuration.x.alternate_domains = tmp
|
||||
end
|
||||
|
||||
it 'returns JSON when account can be found' do
|
||||
get :show, params: { resource: alice.to_webfinger_s }, format: :json
|
||||
|
||||
json = body_as_json
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.content_type).to eq 'application/jrd+json'
|
||||
expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io'
|
||||
expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
|
||||
subject do
|
||||
get :show, params: { resource: resource }, format: :json
|
||||
end
|
||||
|
||||
it 'returns http not found when account cannot be found' do
|
||||
get :show, params: { resource: 'acct:not@existing.com' }, format: :json
|
||||
shared_examples 'a successful response' do
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
it 'returns application/jrd+json' do
|
||||
expect(response.content_type).to eq 'application/jrd+json'
|
||||
end
|
||||
|
||||
it 'returns links for the account' do
|
||||
json = body_as_json
|
||||
expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io'
|
||||
expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns JSON when account can be found with alternate domains' do
|
||||
Rails.configuration.x.alternate_domains = ['foo.org']
|
||||
username, = alice.to_webfinger_s.split('@')
|
||||
context 'when an account exists' do
|
||||
let(:resource) { alice.to_webfinger_s }
|
||||
|
||||
get :show, params: { resource: "#{username}@foo.org" }, format: :json
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
json = body_as_json
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.content_type).to eq 'application/jrd+json'
|
||||
expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io'
|
||||
expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
|
||||
it_behaves_like 'a successful response'
|
||||
end
|
||||
|
||||
it 'returns http not found when account can not be found with alternate domains' do
|
||||
Rails.configuration.x.alternate_domains = ['foo.org']
|
||||
username, = alice.to_webfinger_s.split('@')
|
||||
context 'when an account is temporarily suspended' do
|
||||
let(:resource) { alice.to_webfinger_s }
|
||||
|
||||
get :show, params: { resource: "#{username}@bar.org" }, format: :json
|
||||
before do
|
||||
alice.suspend!
|
||||
subject
|
||||
end
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
it_behaves_like 'a successful response'
|
||||
end
|
||||
|
||||
it 'returns http bad request when not given a resource parameter' do
|
||||
get :show, params: { }, format: :json
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
context 'when an account is permanently suspended or deleted' do
|
||||
let(:resource) { alice.to_webfinger_s }
|
||||
|
||||
before do
|
||||
alice.suspend!
|
||||
alice.deletion_request.destroy
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns http bad request when given a nonsense parameter' do
|
||||
get :show, params: { resource: 'df/:dfkj' }
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
context 'when an account is not found' do
|
||||
let(:resource) { 'acct:not@existing.com' }
|
||||
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an alternate domain' do
|
||||
let(:alternate_domains) { ['foo.org'] }
|
||||
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
context 'when an account exists' do
|
||||
let(:resource) do
|
||||
username, = alice.to_webfinger_s.split('@')
|
||||
"#{username}@foo.org"
|
||||
end
|
||||
|
||||
it_behaves_like 'a successful response'
|
||||
end
|
||||
|
||||
context 'when the domain is wrong' do
|
||||
let(:resource) do
|
||||
username, = alice.to_webfinger_s.split('@')
|
||||
"#{username}@bar.org"
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no resource parameter' do
|
||||
let(:resource) { nil }
|
||||
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns http bad request' do
|
||||
expect(response).to have_http_status(400)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a nonsense parameter' do
|
||||
let(:resource) { 'df/:dfkj' }
|
||||
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns http bad request' do
|
||||
expect(response).to have_http_status(400)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
3
spec/fabricators/account_deletion_request_fabricator.rb
Normal file
3
spec/fabricators/account_deletion_request_fabricator.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
Fabricator(:account_deletion_request) do
|
||||
account
|
||||
end
|
6
spec/fabricators/ip_block_fabricator.rb
Normal file
6
spec/fabricators/ip_block_fabricator.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
Fabricator(:ip_block) do
|
||||
ip ""
|
||||
severity ""
|
||||
expires_at "2020-10-08 22:20:37"
|
||||
comment "MyText"
|
||||
end
|
7
spec/fabricators/webauthn_credential_fabricator.rb
Normal file
7
spec/fabricators/webauthn_credential_fabricator.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
Fabricator(:webauthn_credential) do
|
||||
user_id { Fabricate(:user).id }
|
||||
external_id { Base64.urlsafe_encode64(SecureRandom.random_bytes(16)) }
|
||||
public_key { OpenSSL::PKey::EC.new("prime256v1").generate_key.public_key }
|
||||
nickname 'USB key'
|
||||
sign_count 0
|
||||
end
|
4
spec/fixtures/files/bookmark-imports.txt
vendored
Normal file
4
spec/fixtures/files/bookmark-imports.txt
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
https://example.com/statuses/1312
|
||||
https://local.com/users/foo/statuses/42
|
||||
https://unknown-remote.com/users/bar/statuses/1
|
||||
https://example.com/statuses/direct
|
|
@ -149,22 +149,4 @@ RSpec.describe StatusesHelper, type: :helper do
|
|||
expect(css_class).to eq 'h-cite'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#rtl?' do
|
||||
it 'is false if text is empty' do
|
||||
expect(helper).not_to be_rtl ''
|
||||
end
|
||||
|
||||
it 'is false if there are no right to left characters' do
|
||||
expect(helper).not_to be_rtl 'hello world'
|
||||
end
|
||||
|
||||
it 'is false if right to left characters are fewer than 1/3 of total text' do
|
||||
expect(helper).not_to be_rtl 'hello ݟ world'
|
||||
end
|
||||
|
||||
it 'is true if right to left characters are greater than 1/3 of total text' do
|
||||
expect(helper).to be_rtl 'aaݟaaݟ'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -73,6 +73,26 @@ RSpec.describe ActivityPub::Activity::Announce do
|
|||
expect(sender.reblogged?(sender.statuses.first)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'self-boost of a previously unknown status with correct attributedTo, inlined Collection in audience' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'https://example.com/actor#bar',
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
attributedTo: 'https://example.com/actor',
|
||||
to: {
|
||||
'type': 'OrderedCollection',
|
||||
'id': 'http://example.com/followers',
|
||||
'first': 'http://example.com/followers?page=true',
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a reblog by sender of status' do
|
||||
expect(sender.reblogged?(sender.statuses.first)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status belongs to a local user' do
|
||||
|
|
|
@ -28,6 +28,28 @@ RSpec.describe ActivityPub::Activity::Block do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the recipient is already blocked' do
|
||||
before do
|
||||
sender.block!(recipient, uri: 'old')
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'creates a block from sender to recipient' do
|
||||
expect(sender.blocking?(recipient)).to be true
|
||||
end
|
||||
|
||||
it 'sets the uri to that of last received block activity' do
|
||||
expect(sender.block_relationships.find_by(target_account: recipient).uri).to eq 'foo'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the recipient follows the sender' do
|
||||
before do
|
||||
recipient.follow!(sender)
|
||||
|
|
|
@ -18,6 +18,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
|
||||
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
|
||||
stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
|
||||
stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' })
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
|
@ -120,6 +121,28 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
end
|
||||
|
||||
context 'private with inlined Collection in audience' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
to: {
|
||||
'type': 'OrderedCollection',
|
||||
'id': 'http://example.com/followers',
|
||||
'first': 'http://example.com/followers?page=true',
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.visibility).to eq 'private'
|
||||
end
|
||||
end
|
||||
|
||||
context 'limited' do
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
|
||||
|
@ -451,6 +474,32 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with emojis served with invalid content-type' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum :tinkong:',
|
||||
tag: [
|
||||
{
|
||||
type: 'Emoji',
|
||||
icon: {
|
||||
url: 'http://example.com/emojib.png',
|
||||
},
|
||||
name: 'tinkong',
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.emojis.map(&:shortcode)).to include('tinkong')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with emojis missing name' do
|
||||
let(:object_json) do
|
||||
{
|
||||
|
|
|
@ -3,6 +3,14 @@ require 'rails_helper'
|
|||
RSpec.describe ActivityPub::Activity::Reject do
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'bar',
|
||||
type: 'Follow',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
object: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
}
|
||||
end
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
|
@ -10,29 +18,105 @@ RSpec.describe ActivityPub::Activity::Reject do
|
|||
id: 'foo',
|
||||
type: 'Reject',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: {
|
||||
id: 'bar',
|
||||
type: 'Follow',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
object: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
},
|
||||
object: object_json,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
Fabricate(:follow_request, account: recipient, target_account: sender)
|
||||
subject.perform
|
||||
context 'rejecting a pending follow request by target' do
|
||||
before do
|
||||
Fabricate(:follow_request, account: recipient, target_account: sender)
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'does not create a follow relationship' do
|
||||
expect(recipient.following?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'removes the follow request' do
|
||||
expect(recipient.requested?(sender)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not create a follow relationship' do
|
||||
expect(recipient.following?(sender)).to be false
|
||||
context 'rejecting a pending follow request by uri' do
|
||||
before do
|
||||
Fabricate(:follow_request, account: recipient, target_account: sender, uri: 'bar')
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'does not create a follow relationship' do
|
||||
expect(recipient.following?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'removes the follow request' do
|
||||
expect(recipient.requested?(sender)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
it 'removes the follow request' do
|
||||
expect(recipient.requested?(sender)).to be false
|
||||
context 'rejecting a pending follow request by uri only' do
|
||||
let(:object_json) { 'bar' }
|
||||
|
||||
before do
|
||||
Fabricate(:follow_request, account: recipient, target_account: sender, uri: 'bar')
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'does not create a follow relationship' do
|
||||
expect(recipient.following?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'removes the follow request' do
|
||||
expect(recipient.requested?(sender)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'rejecting an existing follow relationship by target' do
|
||||
before do
|
||||
Fabricate(:follow, account: recipient, target_account: sender)
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'removes the follow relationship' do
|
||||
expect(recipient.following?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'does not create a follow request' do
|
||||
expect(recipient.requested?(sender)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'rejecting an existing follow relationship by uri' do
|
||||
before do
|
||||
Fabricate(:follow, account: recipient, target_account: sender, uri: 'bar')
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'removes the follow relationship' do
|
||||
expect(recipient.following?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'does not create a follow request' do
|
||||
expect(recipient.requested?(sender)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'rejecting an existing follow relationship by uri only' do
|
||||
let(:object_json) { 'bar' }
|
||||
|
||||
before do
|
||||
Fabricate(:follow, account: recipient, target_account: sender, uri: 'bar')
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'removes the follow relationship' do
|
||||
expect(recipient.following?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'does not create a follow request' do
|
||||
expect(recipient.requested?(sender)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -50,6 +50,19 @@ RSpec.describe ActivityPub::Activity::Undo do
|
|||
expect(sender.reblogged?(status)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'with only object uri' do
|
||||
let(:object_json) { 'bar' }
|
||||
|
||||
before do
|
||||
Fabricate(:status, reblog: status, account: sender, uri: 'bar')
|
||||
end
|
||||
|
||||
it 'deletes the reblog by uri' do
|
||||
subject.perform
|
||||
expect(sender.reblogged?(status)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Accept' do
|
||||
|
@ -91,13 +104,22 @@ RSpec.describe ActivityPub::Activity::Undo do
|
|||
end
|
||||
|
||||
before do
|
||||
sender.block!(recipient)
|
||||
sender.block!(recipient, uri: 'bar')
|
||||
end
|
||||
|
||||
it 'deletes block from sender to recipient' do
|
||||
subject.perform
|
||||
expect(sender.blocking?(recipient)).to be false
|
||||
end
|
||||
|
||||
context 'with only object uri' do
|
||||
let(:object_json) { 'bar' }
|
||||
|
||||
it 'deletes block from sender to recipient' do
|
||||
subject.perform
|
||||
expect(sender.blocking?(recipient)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Follow' do
|
||||
|
@ -113,13 +135,22 @@ RSpec.describe ActivityPub::Activity::Undo do
|
|||
end
|
||||
|
||||
before do
|
||||
sender.follow!(recipient)
|
||||
sender.follow!(recipient, uri: 'bar')
|
||||
end
|
||||
|
||||
it 'deletes follow from sender to recipient' do
|
||||
subject.perform
|
||||
expect(sender.following?(recipient)).to be false
|
||||
end
|
||||
|
||||
context 'with only object uri' do
|
||||
let(:object_json) { 'bar' }
|
||||
|
||||
it 'deletes follow from sender to recipient' do
|
||||
subject.perform
|
||||
expect(sender.following?(recipient)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Like' do
|
||||
|
|
73
spec/lib/activitypub/dereferencer_spec.rb
Normal file
73
spec/lib/activitypub/dereferencer_spec.rb
Normal file
|
@ -0,0 +1,73 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Dereferencer do
|
||||
describe '#object' do
|
||||
let(:object) { { '@context': 'https://www.w3.org/ns/activitystreams', id: 'https://example.com/foo', type: 'Note', content: 'Hoge' } }
|
||||
let(:permitted_origin) { 'https://example.com' }
|
||||
let(:signature_account) { nil }
|
||||
let(:uri) { nil }
|
||||
|
||||
subject { described_class.new(uri, permitted_origin: permitted_origin, signature_account: signature_account).object }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/foo').to_return(body: Oj.dump(object), headers: { 'Content-Type' => 'application/activity+json' })
|
||||
end
|
||||
|
||||
context 'with a URI' do
|
||||
let(:uri) { 'https://example.com/foo' }
|
||||
|
||||
it 'returns object' do
|
||||
expect(subject.with_indifferent_access).to eq object.with_indifferent_access
|
||||
end
|
||||
|
||||
context 'with signature account' do
|
||||
let(:signature_account) { Fabricate(:account) }
|
||||
|
||||
it 'makes signed request' do
|
||||
subject
|
||||
expect(a_request(:get, 'https://example.com/foo').with { |req| req.headers['Signature'].present? }).to have_been_made
|
||||
end
|
||||
end
|
||||
|
||||
context 'with different origin' do
|
||||
let(:uri) { 'https://other-example.com/foo' }
|
||||
|
||||
it 'does not make request' do
|
||||
subject
|
||||
expect(a_request(:get, 'https://other-example.com/foo')).to_not have_been_made
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a bearcap' do
|
||||
let(:uri) { 'bear:?t=hoge&u=https://example.com/foo' }
|
||||
|
||||
it 'makes request with Authorization header' do
|
||||
subject
|
||||
expect(a_request(:get, 'https://example.com/foo').with(headers: { 'Authorization' => 'Bearer hoge' })).to have_been_made
|
||||
end
|
||||
|
||||
it 'returns object' do
|
||||
expect(subject.with_indifferent_access).to eq object.with_indifferent_access
|
||||
end
|
||||
|
||||
context 'with signature account' do
|
||||
let(:signature_account) { Fabricate(:account) }
|
||||
|
||||
it 'makes signed request' do
|
||||
subject
|
||||
expect(a_request(:get, 'https://example.com/foo').with { |req| req.headers['Signature'].present? && req.headers['Authorization'] == 'Bearer hoge' }).to have_been_made
|
||||
end
|
||||
end
|
||||
|
||||
context 'with different origin' do
|
||||
let(:uri) { 'bear:?t=hoge&u=https://other-example.com/foo' }
|
||||
|
||||
it 'does not make request' do
|
||||
subject
|
||||
expect(a_request(:get, 'https://other-example.com/foo')).to_not have_been_made
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
21
spec/lib/fast_ip_map_spec.rb
Normal file
21
spec/lib/fast_ip_map_spec.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe FastIpMap do
|
||||
describe '#include?' do
|
||||
subject { described_class.new([IPAddr.new('20.4.0.0/16'), IPAddr.new('145.22.30.0/24'), IPAddr.new('189.45.86.3')])}
|
||||
|
||||
it 'returns true for an exact match' do
|
||||
expect(subject.include?(IPAddr.new('189.45.86.3'))).to be true
|
||||
end
|
||||
|
||||
it 'returns true for a range match' do
|
||||
expect(subject.include?(IPAddr.new('20.4.45.7'))).to be true
|
||||
end
|
||||
|
||||
it 'returns false for no match' do
|
||||
expect(subject.include?(IPAddr.new('145.22.40.64'))).to be false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -29,14 +29,14 @@ RSpec.describe FeedManager do
|
|||
it 'returns false for followee\'s status' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:home, status, bob.id)).to be false
|
||||
expect(FeedManager.instance.filter?(:home, status, bob)).to be false
|
||||
end
|
||||
|
||||
it 'returns false for reblog by followee' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be false
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob)).to be false
|
||||
end
|
||||
|
||||
it 'returns true for reblog by followee of blocked account' do
|
||||
|
@ -44,7 +44,7 @@ RSpec.describe FeedManager do
|
|||
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
bob.block!(jeff)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for reblog by followee of muted account' do
|
||||
|
@ -52,7 +52,7 @@ RSpec.describe FeedManager do
|
|||
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
bob.mute!(jeff)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for reblog by followee of someone who is blocking recipient' do
|
||||
|
@ -60,14 +60,14 @@ RSpec.describe FeedManager do
|
|||
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
jeff.block!(bob)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for reblog from account with reblogs disabled' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reblog = Fabricate(:status, reblog: status, account: alice)
|
||||
bob.follow!(alice, reblogs: false)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns false for reply by followee to another followee' do
|
||||
|
@ -75,48 +75,49 @@ RSpec.describe FeedManager do
|
|||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
bob.follow!(jeff)
|
||||
expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false
|
||||
expect(FeedManager.instance.filter?(:home, reply, bob)).to be false
|
||||
end
|
||||
|
||||
it 'returns false for reply by followee to recipient' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false
|
||||
expect(FeedManager.instance.filter?(:home, reply, bob)).to be false
|
||||
end
|
||||
|
||||
it 'returns false for reply by followee to self' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be false
|
||||
expect(FeedManager.instance.filter?(:home, reply, bob)).to be false
|
||||
end
|
||||
|
||||
it 'returns true for reply by followee to non-followed account' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:home, reply, bob.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:home, reply, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for the second reply by followee to a non-federated status' do
|
||||
reply = Fabricate(:status, text: 'Reply 1', reply: true, account: alice)
|
||||
second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice)
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:home, second_reply, bob.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:home, second_reply, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns false for status by followee mentioning another account' do
|
||||
bob.follow!(alice)
|
||||
jeff.follow!(alice)
|
||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||
expect(FeedManager.instance.filter?(:home, status, bob.id)).to be false
|
||||
expect(FeedManager.instance.filter?(:home, status, bob)).to be false
|
||||
end
|
||||
|
||||
it 'returns true for status by followee mentioning blocked account' do
|
||||
bob.block!(jeff)
|
||||
bob.follow!(alice)
|
||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||
expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:home, status, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for reblog of a personally blocked domain' do
|
||||
|
@ -124,7 +125,7 @@ RSpec.describe FeedManager do
|
|||
alice.follow!(jeff)
|
||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||
reblog = Fabricate(:status, reblog: status, account: jeff)
|
||||
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:home, reblog, alice)).to be true
|
||||
end
|
||||
|
||||
context 'for irreversibly muted phrases' do
|
||||
|
@ -132,7 +133,7 @@ RSpec.describe FeedManager do
|
|||
alice.custom_filters.create!(phrase: 'bob', context: %w(home), irreversible: true)
|
||||
alice.follow!(jeff)
|
||||
status = Fabricate(:status, text: 'bobcats', account: jeff)
|
||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be_falsy
|
||||
expect(FeedManager.instance.filter?(:home, status, alice)).to be_falsy
|
||||
end
|
||||
|
||||
it 'returns true if phrase is contained' do
|
||||
|
@ -140,14 +141,14 @@ RSpec.describe FeedManager do
|
|||
alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true)
|
||||
alice.follow!(jeff)
|
||||
status = Fabricate(:status, text: 'i sure like POP TARts', account: jeff)
|
||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:home, status, alice)).to be true
|
||||
end
|
||||
|
||||
it 'matches substrings if whole_word is false' do
|
||||
alice.custom_filters.create!(phrase: 'take', context: %w(home), whole_word: false, irreversible: true)
|
||||
alice.follow!(jeff)
|
||||
status = Fabricate(:status, text: 'shiitake', account: jeff)
|
||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:home, status, alice)).to be true
|
||||
end
|
||||
|
||||
it 'returns true if phrase is contained in a poll option' do
|
||||
|
@ -155,7 +156,7 @@ RSpec.describe FeedManager do
|
|||
alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true)
|
||||
alice.follow!(jeff)
|
||||
status = Fabricate(:status, text: 'what do you prefer', poll: Fabricate(:poll, options: %w(farts POP TARts)), account: jeff)
|
||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:home, status, alice)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -164,27 +165,27 @@ RSpec.describe FeedManager do
|
|||
it 'returns true for status that mentions blocked account' do
|
||||
bob.block!(jeff)
|
||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:mentions, status, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for status that replies to a blocked account' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: jeff)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
|
||||
bob.block!(jeff)
|
||||
expect(FeedManager.instance.filter?(:mentions, reply, bob.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:mentions, reply, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns true for status by silenced account who recipient is not following' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
alice.silence!
|
||||
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true
|
||||
expect(FeedManager.instance.filter?(:mentions, status, bob)).to be true
|
||||
end
|
||||
|
||||
it 'returns false for status by followed silenced account' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
alice.silence!
|
||||
bob.follow!(alice)
|
||||
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false
|
||||
expect(FeedManager.instance.filter?(:mentions, status, bob)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -309,62 +310,125 @@ RSpec.describe FeedManager do
|
|||
end
|
||||
|
||||
describe '#push_to_list' do
|
||||
let(:owner) { Fabricate(:account, username: 'owner') }
|
||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||
let(:bob) { Fabricate(:account, username: 'bob') }
|
||||
let(:eve) { Fabricate(:account, username: 'eve') }
|
||||
let(:list) { Fabricate(:list, account: owner) }
|
||||
|
||||
before do
|
||||
owner.follow!(alice)
|
||||
owner.follow!(bob)
|
||||
owner.follow!(eve)
|
||||
|
||||
list.accounts << alice
|
||||
list.accounts << bob
|
||||
end
|
||||
|
||||
it "does not push when the given status's reblog is already inserted" do
|
||||
list = Fabricate(:list)
|
||||
reblog = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblog)
|
||||
FeedManager.instance.push_to_list(list, status)
|
||||
|
||||
expect(FeedManager.instance.push_to_list(list, reblog)).to eq false
|
||||
end
|
||||
|
||||
context 'when replies policy is set to no replies' do
|
||||
before do
|
||||
list.replies_policy = :none
|
||||
end
|
||||
|
||||
it 'pushes statuses that are not replies' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||
expect(FeedManager.instance.push_to_list(list, status)).to eq true
|
||||
end
|
||||
|
||||
it 'pushes statuses that are replies to list owner' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: owner)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||
expect(FeedManager.instance.push_to_list(list, reply)).to eq true
|
||||
end
|
||||
|
||||
it 'does not push replies to another member of the list' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||
expect(FeedManager.instance.push_to_list(list, reply)).to eq false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when replies policy is set to list-only replies' do
|
||||
before do
|
||||
list.replies_policy = :list
|
||||
end
|
||||
|
||||
it 'pushes statuses that are not replies' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||
expect(FeedManager.instance.push_to_list(list, status)).to eq true
|
||||
end
|
||||
|
||||
it 'pushes statuses that are replies to list owner' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: owner)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||
expect(FeedManager.instance.push_to_list(list, reply)).to eq true
|
||||
end
|
||||
|
||||
it 'pushes replies to another member of the list' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||
expect(FeedManager.instance.push_to_list(list, reply)).to eq true
|
||||
end
|
||||
|
||||
it 'does not push replies to someone not a member of the list' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: eve)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||
expect(FeedManager.instance.push_to_list(list, reply)).to eq false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when replies policy is set to any reply' do
|
||||
before do
|
||||
list.replies_policy = :followed
|
||||
end
|
||||
|
||||
it 'pushes statuses that are not replies' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: bob)
|
||||
expect(FeedManager.instance.push_to_list(list, status)).to eq true
|
||||
end
|
||||
|
||||
it 'pushes statuses that are replies to list owner' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: owner)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||
expect(FeedManager.instance.push_to_list(list, reply)).to eq true
|
||||
end
|
||||
|
||||
it 'pushes replies to another member of the list' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: alice)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||
expect(FeedManager.instance.push_to_list(list, reply)).to eq true
|
||||
end
|
||||
|
||||
it 'pushes replies to someone not a member of the list' do
|
||||
status = Fabricate(:status, text: 'Hello world', account: eve)
|
||||
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
|
||||
expect(FeedManager.instance.push_to_list(list, reply)).to eq true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#merge_into_timeline' do
|
||||
describe '#merge_into_home' do
|
||||
it "does not push source account's statuses whose reblogs are already inserted" do
|
||||
account = Fabricate(:account, id: 0)
|
||||
reblog = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblog)
|
||||
FeedManager.instance.push_to_home(account, status)
|
||||
|
||||
FeedManager.instance.merge_into_timeline(account, reblog.account)
|
||||
FeedManager.instance.merge_into_home(account, reblog.account)
|
||||
|
||||
expect(Redis.current.zscore("feed:home:0", reblog.id)).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#trim' do
|
||||
let(:receiver) { Fabricate(:account) }
|
||||
|
||||
it 'cleans up reblog tracking keys' do
|
||||
reblogged = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblogged)
|
||||
another_status = Fabricate(:status, reblog: reblogged)
|
||||
reblogs_key = FeedManager.instance.key('home', receiver.id, 'reblogs')
|
||||
reblog_set_key = FeedManager.instance.key('home', receiver.id, "reblogs:#{reblogged.id}")
|
||||
|
||||
FeedManager.instance.push_to_home(receiver, status)
|
||||
FeedManager.instance.push_to_home(receiver, another_status)
|
||||
|
||||
# We should have a tracking set and an entry in reblogs.
|
||||
expect(Redis.current.exists(reblog_set_key)).to be true
|
||||
expect(Redis.current.zrange(reblogs_key, 0, -1)).to eq [reblogged.id.to_s]
|
||||
|
||||
# Push everything off the end of the feed.
|
||||
FeedManager::MAX_ITEMS.times do
|
||||
FeedManager.instance.push_to_home(receiver, Fabricate(:status))
|
||||
end
|
||||
|
||||
# `trim` should be called automatically, but do it anyway, as
|
||||
# we're testing `trim`, not side effects of `push`.
|
||||
FeedManager.instance.trim('home', receiver.id)
|
||||
|
||||
# We should not have any reblog tracking data.
|
||||
expect(Redis.current.exists(reblog_set_key)).to be false
|
||||
expect(Redis.current.zrange(reblogs_key, 0, -1)).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unpush' do
|
||||
describe '#unpush_from_home' do
|
||||
let(:receiver) { Fabricate(:account) }
|
||||
|
||||
it 'leaves a reblogged status if original was on feed' do
|
||||
|
@ -430,7 +494,7 @@ RSpec.describe FeedManager do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#clear_from_timeline' do
|
||||
describe '#clear_from_home' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:followed_account) { Fabricate(:account) }
|
||||
let(:target_account) { Fabricate(:account) }
|
||||
|
@ -448,8 +512,8 @@ RSpec.describe FeedManager do
|
|||
end
|
||||
end
|
||||
|
||||
it 'correctly cleans the timeline' do
|
||||
FeedManager.instance.clear_from_timeline(account, target_account)
|
||||
it 'correctly cleans the home timeline' do
|
||||
FeedManager.instance.clear_from_home(account, target_account)
|
||||
|
||||
expect(Redis.current.zrange("feed:home:#{account.id}", 0, -1)).to eq [status_1.id.to_s, status_7.id.to_s]
|
||||
end
|
||||
|
|
|
@ -150,9 +150,9 @@ RSpec.describe SpamCheck do
|
|||
let(:redis_key) { spam_check.send(:redis_key) }
|
||||
|
||||
it 'remembers' do
|
||||
expect(Redis.current.exists(redis_key)).to be true
|
||||
expect(Redis.current.exists?(redis_key)).to be true
|
||||
spam_check.remember!
|
||||
expect(Redis.current.exists(redis_key)).to be true
|
||||
expect(Redis.current.exists?(redis_key)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -166,9 +166,9 @@ RSpec.describe SpamCheck do
|
|||
end
|
||||
|
||||
it 'resets' do
|
||||
expect(Redis.current.exists(redis_key)).to be true
|
||||
expect(Redis.current.exists?(redis_key)).to be true
|
||||
spam_check.reset!
|
||||
expect(Redis.current.exists(redis_key)).to be false
|
||||
expect(Redis.current.exists?(redis_key)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -33,6 +33,28 @@ class UserMailerPreview < ActionMailer::Preview
|
|||
UserMailer.two_factor_recovery_codes_changed(User.first)
|
||||
end
|
||||
|
||||
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/webauthn_enabled
|
||||
def webauthn_enabled
|
||||
UserMailer.webauthn_enabled(User.first)
|
||||
end
|
||||
|
||||
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/webauthn_disabled
|
||||
def webauthn_disabled
|
||||
UserMailer.webauthn_disabled(User.first)
|
||||
end
|
||||
|
||||
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/webauthn_credential_added
|
||||
def webauthn_credential_added
|
||||
webauthn_credential = WebauthnCredential.new(nickname: 'USB Key')
|
||||
UserMailer.webauthn_credential_added(User.first, webauthn_credential)
|
||||
end
|
||||
|
||||
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/webauthn_credential_deleted
|
||||
def webauthn_credential_deleted
|
||||
webauthn_credential = WebauthnCredential.new(nickname: 'USB Key')
|
||||
UserMailer.webauthn_credential_deleted(User.first, webauthn_credential)
|
||||
end
|
||||
|
||||
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/reconfirmation_instructions
|
||||
def reconfirmation_instructions
|
||||
user = User.first
|
||||
|
|
4
spec/models/account_deletion_request_spec.rb
Normal file
4
spec/models/account_deletion_request_spec.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountDeletionRequest, type: :model do
|
||||
end
|
|
@ -5,7 +5,7 @@ describe AccountFilter do
|
|||
it 'defaults to recent local not-suspended account list' do
|
||||
filter = described_class.new({})
|
||||
|
||||
expect(filter.results).to eq Account.local.recent.without_suspended
|
||||
expect(filter.results).to eq Account.local.without_instance_actor.recent.without_suspended
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -440,13 +440,6 @@ RSpec.describe Account, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.domains' do
|
||||
it 'returns domains' do
|
||||
Fabricate(:account, domain: 'domain')
|
||||
expect(Account.remote.domains).to match_array(['domain'])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#statuses_count' do
|
||||
subject { Fabricate(:account) }
|
||||
|
||||
|
@ -737,20 +730,6 @@ RSpec.describe Account, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'by_domain_accounts' do
|
||||
it 'returns accounts grouped by domain sorted by accounts' do
|
||||
2.times { Fabricate(:account, domain: 'example.com') }
|
||||
Fabricate(:account, domain: 'example2.com')
|
||||
|
||||
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
|
||||
expect(results.last.domain).to eq 'example2.com'
|
||||
expect(results.last.accounts_count).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
describe 'local' do
|
||||
it 'returns an array of accounts who do not have a domain' do
|
||||
account_1 = Fabricate(:account, domain: nil)
|
||||
|
@ -817,4 +796,27 @@ RSpec.describe Account, type: :model do
|
|||
|
||||
include_examples 'AccountAvatar', :account
|
||||
include_examples 'AccountHeader', :account
|
||||
|
||||
describe '#increment_count!' do
|
||||
subject { Fabricate(:account) }
|
||||
|
||||
it 'increments the count in multi-threaded an environment when account_stat is not yet initialized' do
|
||||
subject
|
||||
|
||||
increment_by = 15
|
||||
wait_for_start = true
|
||||
|
||||
threads = Array.new(increment_by) do
|
||||
Thread.new do
|
||||
true while wait_for_start
|
||||
Account.find(subject.id).increment_count!(:followers_count)
|
||||
end
|
||||
end
|
||||
|
||||
wait_for_start = false
|
||||
threads.each(&:join)
|
||||
|
||||
expect(subject.reload.followers_count).to eq 15
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -115,16 +115,16 @@ RSpec.describe Admin::AccountAction, type: :model do
|
|||
context 'account.local?' do
|
||||
let(:account) { Fabricate(:account, domain: nil) }
|
||||
|
||||
it 'returns ["none", "disable", "silence", "suspend"]' do
|
||||
expect(subject).to eq %w(none disable silence suspend)
|
||||
it 'returns ["none", "disable", "sensitive", "silence", "suspend"]' do
|
||||
expect(subject).to eq %w(none disable sensitive silence suspend)
|
||||
end
|
||||
end
|
||||
|
||||
context '!account.local?' do
|
||||
let(:account) { Fabricate(:account, domain: 'hoge.com') }
|
||||
|
||||
it 'returns ["silence", "suspend"]' do
|
||||
expect(subject).to eq %w(silence suspend)
|
||||
it 'returns ["sensitive", "silence", "suspend"]' do
|
||||
expect(subject).to eq %w(sensitive silence suspend)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ describe AccountInteractions do
|
|||
context 'account with Follow' do
|
||||
it 'returns { target_account_id => true }' do
|
||||
Fabricate(:follow, account: account, target_account: target_account)
|
||||
is_expected.to eq(target_account_id => { reblogs: true })
|
||||
is_expected.to eq(target_account_id => { reblogs: true, notify: false })
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -539,6 +539,49 @@ describe AccountInteractions do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#followers_hash' do
|
||||
let(:me) { Fabricate(:account, username: 'Me') }
|
||||
let(:remote_1) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') }
|
||||
let(:remote_2) { Fabricate(:account, username: 'bob', domain: 'example.org', uri: 'https://example.org/users/bob') }
|
||||
let(:remote_3) { Fabricate(:account, username: 'eve', domain: 'foo.org', uri: 'https://foo.org/users/eve') }
|
||||
|
||||
before do
|
||||
remote_1.follow!(me)
|
||||
remote_2.follow!(me)
|
||||
remote_3.follow!(me)
|
||||
me.follow!(remote_1)
|
||||
end
|
||||
|
||||
context 'on a local user' do
|
||||
it 'returns correct hash for remote domains' do
|
||||
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
||||
expect(me.remote_followers_hash('https://foo.org/')).to eq 'ccb9c18a67134cfff9d62c7f7e7eb88e6b803446c244b84265565f4eba29df0e'
|
||||
end
|
||||
|
||||
it 'invalidates cache as needed when removing or adding followers' do
|
||||
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
||||
remote_1.unfollow!(me)
|
||||
expect(me.remote_followers_hash('https://example.org/')).to eq '241b00794ce9b46aa864f3220afadef128318da2659782985bac5ed5bd436bff'
|
||||
remote_1.follow!(me)
|
||||
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
||||
end
|
||||
end
|
||||
|
||||
context 'on a remote user' do
|
||||
it 'returns correct hash for remote domains' do
|
||||
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
||||
end
|
||||
|
||||
it 'invalidates cache as needed when removing or adding followers' do
|
||||
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
||||
me.unfollow!(remote_1)
|
||||
expect(remote_1.local_followers_hash).to eq '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
me.follow!(remote_1)
|
||||
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'muting an account' do
|
||||
let(:me) { Fabricate(:account, username: 'Me') }
|
||||
let(:you) { Fabricate(:account, username: 'You') }
|
||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe FollowRequest, type: :model do
|
|||
let(:target_account) { Fabricate(:account) }
|
||||
|
||||
it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do
|
||||
expect(account).to receive(:follow!).with(target_account, reblogs: true, uri: follow_request.uri)
|
||||
expect(account).to receive(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri)
|
||||
expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id)
|
||||
expect(follow_request).to receive(:destroy!)
|
||||
follow_request.authorize!
|
||||
|
|
|
@ -5,7 +5,7 @@ RSpec.describe Follow, type: :model do
|
|||
let(:bob) { Fabricate(:account, username: 'bob') }
|
||||
|
||||
describe 'validations' do
|
||||
subject { Follow.new(account: alice, target_account: bob) }
|
||||
subject { Follow.new(account: alice, target_account: bob, rate_limit: true) }
|
||||
|
||||
it 'has a valid fabricator' do
|
||||
follow = Fabricate.build(:follow)
|
||||
|
|
|
@ -20,5 +20,15 @@ RSpec.describe Import, type: :model do
|
|||
import = Import.create(account: account, type: type)
|
||||
expect(import).to model_have_error_on_field(:data)
|
||||
end
|
||||
|
||||
it 'is invalid with too many rows in data' do
|
||||
import = Import.create(account: account, type: type, data: StringIO.new("foo@bar.com\n" * (ImportService::ROWS_PROCESSING_LIMIT + 10)))
|
||||
expect(import).to model_have_error_on_field(:data)
|
||||
end
|
||||
|
||||
it 'is invalid when there are more rows when following limit' do
|
||||
import = Import.create(account: account, type: type, data: StringIO.new("foo@bar.com\n" * (FollowLimitValidator.limit_for_account(account) + 10)))
|
||||
expect(import).to model_have_error_on_field(:data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,7 +29,7 @@ RSpec.describe Invite, type: :model do
|
|||
|
||||
it 'returns false when invite creator has been disabled' do
|
||||
invite = Fabricate(:invite, max_uses: nil, expires_at: nil)
|
||||
SuspendAccountService.new.call(invite.user.account)
|
||||
invite.user.account.suspend!
|
||||
expect(invite.valid_for_use?).to be false
|
||||
end
|
||||
end
|
||||
|
|
5
spec/models/ip_block_spec.rb
Normal file
5
spec/models/ip_block_spec.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe IpBlock, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
212
spec/models/public_feed_spec.rb
Normal file
212
spec/models/public_feed_spec.rb
Normal file
|
@ -0,0 +1,212 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PublicFeed, type: :model do
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
describe '#get' do
|
||||
subject { described_class.new(nil).get(20).map(&:id) }
|
||||
|
||||
it 'only includes statuses with public visibility' do
|
||||
public_status = Fabricate(:status, visibility: :public)
|
||||
private_status = Fabricate(:status, visibility: :private)
|
||||
|
||||
expect(subject).to include(public_status.id)
|
||||
expect(subject).not_to include(private_status.id)
|
||||
end
|
||||
|
||||
it 'does not include replies' do
|
||||
status = Fabricate(:status)
|
||||
reply = Fabricate(:status, in_reply_to_id: status.id)
|
||||
|
||||
expect(subject).to include(status.id)
|
||||
expect(subject).not_to include(reply.id)
|
||||
end
|
||||
|
||||
it 'does not include boosts' do
|
||||
status = Fabricate(:status)
|
||||
boost = Fabricate(:status, reblog_of_id: status.id)
|
||||
|
||||
expect(subject).to include(status.id)
|
||||
expect(subject).not_to include(boost.id)
|
||||
end
|
||||
|
||||
it 'filters out silenced accounts' do
|
||||
account = Fabricate(:account)
|
||||
silenced_account = Fabricate(:account, silenced: true)
|
||||
status = Fabricate(:status, account: account)
|
||||
silenced_status = Fabricate(:status, account: silenced_account)
|
||||
|
||||
expect(subject).to include(status.id)
|
||||
expect(subject).not_to include(silenced_status.id)
|
||||
end
|
||||
|
||||
context 'without local_only option' do
|
||||
let(:viewer) { nil }
|
||||
|
||||
let!(:local_account) { Fabricate(:account, domain: nil) }
|
||||
let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
|
||||
let!(:local_status) { Fabricate(:status, account: local_account) }
|
||||
let!(:remote_status) { Fabricate(:status, account: remote_account) }
|
||||
|
||||
subject { described_class.new(viewer).get(20).map(&:id) }
|
||||
|
||||
context 'without a viewer' do
|
||||
let(:viewer) { nil }
|
||||
|
||||
it 'includes remote instances statuses' do
|
||||
expect(subject).to include(remote_status.id)
|
||||
end
|
||||
|
||||
it 'includes local statuses' do
|
||||
expect(subject).to include(local_status.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a viewer' do
|
||||
let(:viewer) { Fabricate(:account, username: 'viewer') }
|
||||
|
||||
it 'includes remote instances statuses' do
|
||||
expect(subject).to include(remote_status.id)
|
||||
end
|
||||
|
||||
it 'includes local statuses' do
|
||||
expect(subject).to include(local_status.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a local_only option set' do
|
||||
let!(:local_account) { Fabricate(:account, domain: nil) }
|
||||
let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
|
||||
let!(:local_status) { Fabricate(:status, account: local_account) }
|
||||
let!(:remote_status) { Fabricate(:status, account: remote_account) }
|
||||
|
||||
subject { described_class.new(viewer, local: true).get(20).map(&:id) }
|
||||
|
||||
context 'without a viewer' do
|
||||
let(:viewer) { nil }
|
||||
|
||||
it 'does not include remote instances statuses' do
|
||||
expect(subject).to include(local_status.id)
|
||||
expect(subject).not_to include(remote_status.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a viewer' do
|
||||
let(:viewer) { Fabricate(:account, username: 'viewer') }
|
||||
|
||||
it 'does not include remote instances statuses' do
|
||||
expect(subject).to include(local_status.id)
|
||||
expect(subject).not_to include(remote_status.id)
|
||||
end
|
||||
|
||||
it 'is not affected by personal domain blocks' do
|
||||
viewer.block_domain!('test.com')
|
||||
expect(subject).to include(local_status.id)
|
||||
expect(subject).not_to include(remote_status.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a remote_only option set' do
|
||||
let!(:local_account) { Fabricate(:account, domain: nil) }
|
||||
let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
|
||||
let!(:local_status) { Fabricate(:status, account: local_account) }
|
||||
let!(:remote_status) { Fabricate(:status, account: remote_account) }
|
||||
|
||||
subject { described_class.new(viewer, remote: true).get(20).map(&:id) }
|
||||
|
||||
context 'without a viewer' do
|
||||
let(:viewer) { nil }
|
||||
|
||||
it 'does not include local instances statuses' do
|
||||
expect(subject).not_to include(local_status.id)
|
||||
expect(subject).to include(remote_status.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a viewer' do
|
||||
let(:viewer) { Fabricate(:account, username: 'viewer') }
|
||||
|
||||
it 'does not include local instances statuses' do
|
||||
expect(subject).not_to include(local_status.id)
|
||||
expect(subject).to include(remote_status.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with an account passed in' do
|
||||
before do
|
||||
@account = Fabricate(:account)
|
||||
end
|
||||
|
||||
subject { described_class.new(@account).get(20).map(&:id) }
|
||||
|
||||
it 'excludes statuses from accounts blocked by the account' do
|
||||
blocked = Fabricate(:account)
|
||||
@account.block!(blocked)
|
||||
blocked_status = Fabricate(:status, account: blocked)
|
||||
|
||||
expect(subject).not_to include(blocked_status.id)
|
||||
end
|
||||
|
||||
it 'excludes statuses from accounts who have blocked the account' do
|
||||
blocker = Fabricate(:account)
|
||||
blocker.block!(@account)
|
||||
blocked_status = Fabricate(:status, account: blocker)
|
||||
|
||||
expect(subject).not_to include(blocked_status.id)
|
||||
end
|
||||
|
||||
it 'excludes statuses from accounts muted by the account' do
|
||||
muted = Fabricate(:account)
|
||||
@account.mute!(muted)
|
||||
muted_status = Fabricate(:status, account: muted)
|
||||
|
||||
expect(subject).not_to include(muted_status.id)
|
||||
end
|
||||
|
||||
it 'excludes statuses from accounts from personally blocked domains' do
|
||||
blocked = Fabricate(:account, domain: 'example.com')
|
||||
@account.block_domain!(blocked.domain)
|
||||
blocked_status = Fabricate(:status, account: blocked)
|
||||
|
||||
expect(subject).not_to include(blocked_status.id)
|
||||
end
|
||||
|
||||
context 'with language preferences' do
|
||||
it 'excludes statuses in languages not allowed by the account user' do
|
||||
user = Fabricate(:user, chosen_languages: [:en, :es])
|
||||
@account.update(user: user)
|
||||
en_status = Fabricate(:status, language: 'en')
|
||||
es_status = Fabricate(:status, language: 'es')
|
||||
fr_status = Fabricate(:status, language: 'fr')
|
||||
|
||||
expect(subject).to include(en_status.id)
|
||||
expect(subject).to include(es_status.id)
|
||||
expect(subject).not_to include(fr_status.id)
|
||||
end
|
||||
|
||||
it 'includes all languages when user does not have a setting' do
|
||||
user = Fabricate(:user, chosen_languages: nil)
|
||||
@account.update(user: user)
|
||||
|
||||
en_status = Fabricate(:status, language: 'en')
|
||||
es_status = Fabricate(:status, language: 'es')
|
||||
|
||||
expect(subject).to include(en_status.id)
|
||||
expect(subject).to include(es_status.id)
|
||||
end
|
||||
|
||||
it 'includes all languages when account does not have a user' do
|
||||
expect(@account.user).to be_nil
|
||||
en_status = Fabricate(:status, language: 'en')
|
||||
es_status = Fabricate(:status, language: 'es')
|
||||
|
||||
expect(subject).to include(en_status.id)
|
||||
expect(subject).to include(es_status.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -267,241 +267,6 @@ RSpec.describe Status, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.as_public_timeline' do
|
||||
it 'only includes statuses with public visibility' do
|
||||
public_status = Fabricate(:status, visibility: :public)
|
||||
private_status = Fabricate(:status, visibility: :private)
|
||||
|
||||
results = Status.as_public_timeline
|
||||
expect(results).to include(public_status)
|
||||
expect(results).not_to include(private_status)
|
||||
end
|
||||
|
||||
it 'does not include replies' do
|
||||
status = Fabricate(:status)
|
||||
reply = Fabricate(:status, in_reply_to_id: status.id)
|
||||
|
||||
results = Status.as_public_timeline
|
||||
expect(results).to include(status)
|
||||
expect(results).not_to include(reply)
|
||||
end
|
||||
|
||||
it 'does not include boosts' do
|
||||
status = Fabricate(:status)
|
||||
boost = Fabricate(:status, reblog_of_id: status.id)
|
||||
|
||||
results = Status.as_public_timeline
|
||||
expect(results).to include(status)
|
||||
expect(results).not_to include(boost)
|
||||
end
|
||||
|
||||
it 'filters out silenced accounts' do
|
||||
account = Fabricate(:account)
|
||||
silenced_account = Fabricate(:account, silenced: true)
|
||||
status = Fabricate(:status, account: account)
|
||||
silenced_status = Fabricate(:status, account: silenced_account)
|
||||
|
||||
results = Status.as_public_timeline
|
||||
expect(results).to include(status)
|
||||
expect(results).not_to include(silenced_status)
|
||||
end
|
||||
|
||||
context 'without local_only option' do
|
||||
let(:viewer) { nil }
|
||||
|
||||
let!(:local_account) { Fabricate(:account, domain: nil) }
|
||||
let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
|
||||
let!(:local_status) { Fabricate(:status, account: local_account) }
|
||||
let!(:remote_status) { Fabricate(:status, account: remote_account) }
|
||||
|
||||
subject { Status.as_public_timeline(viewer, false) }
|
||||
|
||||
context 'without a viewer' do
|
||||
let(:viewer) { nil }
|
||||
|
||||
it 'includes remote instances statuses' do
|
||||
expect(subject).to include(remote_status)
|
||||
end
|
||||
|
||||
it 'includes local statuses' do
|
||||
expect(subject).to include(local_status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a viewer' do
|
||||
let(:viewer) { Fabricate(:account, username: 'viewer') }
|
||||
|
||||
it 'includes remote instances statuses' do
|
||||
expect(subject).to include(remote_status)
|
||||
end
|
||||
|
||||
it 'includes local statuses' do
|
||||
expect(subject).to include(local_status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a local_only option set' do
|
||||
let!(:local_account) { Fabricate(:account, domain: nil) }
|
||||
let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
|
||||
let!(:local_status) { Fabricate(:status, account: local_account) }
|
||||
let!(:remote_status) { Fabricate(:status, account: remote_account) }
|
||||
|
||||
subject { Status.as_public_timeline(viewer, true) }
|
||||
|
||||
context 'without a viewer' do
|
||||
let(:viewer) { nil }
|
||||
|
||||
it 'does not include remote instances statuses' do
|
||||
expect(subject).to include(local_status)
|
||||
expect(subject).not_to include(remote_status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a viewer' do
|
||||
let(:viewer) { Fabricate(:account, username: 'viewer') }
|
||||
|
||||
it 'does not include remote instances statuses' do
|
||||
expect(subject).to include(local_status)
|
||||
expect(subject).not_to include(remote_status)
|
||||
end
|
||||
|
||||
it 'is not affected by personal domain blocks' do
|
||||
viewer.block_domain!('test.com')
|
||||
expect(subject).to include(local_status)
|
||||
expect(subject).not_to include(remote_status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a remote_only option set' do
|
||||
let!(:local_account) { Fabricate(:account, domain: nil) }
|
||||
let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
|
||||
let!(:local_status) { Fabricate(:status, account: local_account) }
|
||||
let!(:remote_status) { Fabricate(:status, account: remote_account) }
|
||||
|
||||
subject { Status.as_public_timeline(viewer, :remote) }
|
||||
|
||||
context 'without a viewer' do
|
||||
let(:viewer) { nil }
|
||||
|
||||
it 'does not include local instances statuses' do
|
||||
expect(subject).not_to include(local_status)
|
||||
expect(subject).to include(remote_status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a viewer' do
|
||||
let(:viewer) { Fabricate(:account, username: 'viewer') }
|
||||
|
||||
it 'does not include local instances statuses' do
|
||||
expect(subject).not_to include(local_status)
|
||||
expect(subject).to include(remote_status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with an account passed in' do
|
||||
before do
|
||||
@account = Fabricate(:account)
|
||||
end
|
||||
|
||||
it 'excludes statuses from accounts blocked by the account' do
|
||||
blocked = Fabricate(:account)
|
||||
Fabricate(:block, account: @account, target_account: blocked)
|
||||
blocked_status = Fabricate(:status, account: blocked)
|
||||
|
||||
results = Status.as_public_timeline(@account)
|
||||
expect(results).not_to include(blocked_status)
|
||||
end
|
||||
|
||||
it 'excludes statuses from accounts who have blocked the account' do
|
||||
blocked = Fabricate(:account)
|
||||
Fabricate(:block, account: blocked, target_account: @account)
|
||||
blocked_status = Fabricate(:status, account: blocked)
|
||||
|
||||
results = Status.as_public_timeline(@account)
|
||||
expect(results).not_to include(blocked_status)
|
||||
end
|
||||
|
||||
it 'excludes statuses from accounts muted by the account' do
|
||||
muted = Fabricate(:account)
|
||||
Fabricate(:mute, account: @account, target_account: muted)
|
||||
muted_status = Fabricate(:status, account: muted)
|
||||
|
||||
results = Status.as_public_timeline(@account)
|
||||
expect(results).not_to include(muted_status)
|
||||
end
|
||||
|
||||
it 'excludes statuses from accounts from personally blocked domains' do
|
||||
blocked = Fabricate(:account, domain: 'example.com')
|
||||
@account.block_domain!(blocked.domain)
|
||||
blocked_status = Fabricate(:status, account: blocked)
|
||||
|
||||
results = Status.as_public_timeline(@account)
|
||||
expect(results).not_to include(blocked_status)
|
||||
end
|
||||
|
||||
context 'with language preferences' do
|
||||
it 'excludes statuses in languages not allowed by the account user' do
|
||||
user = Fabricate(:user, chosen_languages: [:en, :es])
|
||||
@account.update(user: user)
|
||||
en_status = Fabricate(:status, language: 'en')
|
||||
es_status = Fabricate(:status, language: 'es')
|
||||
fr_status = Fabricate(:status, language: 'fr')
|
||||
|
||||
results = Status.as_public_timeline(@account)
|
||||
expect(results).to include(en_status)
|
||||
expect(results).to include(es_status)
|
||||
expect(results).not_to include(fr_status)
|
||||
end
|
||||
|
||||
it 'includes all languages when user does not have a setting' do
|
||||
user = Fabricate(:user, chosen_languages: nil)
|
||||
@account.update(user: user)
|
||||
|
||||
en_status = Fabricate(:status, language: 'en')
|
||||
es_status = Fabricate(:status, language: 'es')
|
||||
|
||||
results = Status.as_public_timeline(@account)
|
||||
expect(results).to include(en_status)
|
||||
expect(results).to include(es_status)
|
||||
end
|
||||
|
||||
it 'includes all languages when account does not have a user' do
|
||||
expect(@account.user).to be_nil
|
||||
en_status = Fabricate(:status, language: 'en')
|
||||
es_status = Fabricate(:status, language: 'es')
|
||||
|
||||
results = Status.as_public_timeline(@account)
|
||||
expect(results).to include(en_status)
|
||||
expect(results).to include(es_status)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.as_tag_timeline' do
|
||||
it 'includes statuses with a tag' do
|
||||
tag = Fabricate(:tag)
|
||||
status = Fabricate(:status, tags: [tag])
|
||||
other = Fabricate(:status)
|
||||
|
||||
results = Status.as_tag_timeline(tag)
|
||||
expect(results).to include(status)
|
||||
expect(results).not_to include(other)
|
||||
end
|
||||
|
||||
it 'allows replies to be included' do
|
||||
original = Fabricate(:status)
|
||||
tag = Fabricate(:tag)
|
||||
status = Fabricate(:status, tags: [tag], in_reply_to_id: original.id)
|
||||
|
||||
results = Status.as_tag_timeline(tag)
|
||||
expect(results).to include(status)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.permitted_for' do
|
||||
subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe HashtagQueryService, type: :service do
|
||||
describe '.call' do
|
||||
describe TagFeed, type: :service do
|
||||
describe '#get' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:tag1) { Fabricate(:tag) }
|
||||
let(:tag2) { Fabricate(:tag) }
|
||||
|
@ -10,35 +10,35 @@ describe HashtagQueryService, type: :service do
|
|||
let!(:both) { Fabricate(:status, tags: [tag1, tag2]) }
|
||||
|
||||
it 'can add tags in "any" mode' do
|
||||
results = subject.call(tag1, { any: [tag2.name] })
|
||||
results = described_class.new(tag1, nil, any: [tag2.name]).get(20)
|
||||
expect(results).to include status1
|
||||
expect(results).to include status2
|
||||
expect(results).to include both
|
||||
end
|
||||
|
||||
it 'can remove tags in "all" mode' do
|
||||
results = subject.call(tag1, { all: [tag2.name] })
|
||||
results = described_class.new(tag1, nil, all: [tag2.name]).get(20)
|
||||
expect(results).to_not include status1
|
||||
expect(results).to_not include status2
|
||||
expect(results).to include both
|
||||
end
|
||||
|
||||
it 'can remove tags in "none" mode' do
|
||||
results = subject.call(tag1, { none: [tag2.name] })
|
||||
results = described_class.new(tag1, nil, none: [tag2.name]).get(20)
|
||||
expect(results).to include status1
|
||||
expect(results).to_not include status2
|
||||
expect(results).to_not include both
|
||||
end
|
||||
|
||||
it 'ignores an invalid mode' do
|
||||
results = subject.call(tag1, { wark: [tag2.name] })
|
||||
results = described_class.new(tag1, nil, wark: [tag2.name]).get(20)
|
||||
expect(results).to include status1
|
||||
expect(results).to_not include status2
|
||||
expect(results).to include both
|
||||
end
|
||||
|
||||
it 'handles being passed non existant tag names' do
|
||||
results = subject.call(tag1, { any: ['wark'] })
|
||||
results = described_class.new(tag1, nil, any: ['wark']).get(20)
|
||||
expect(results).to include status1
|
||||
expect(results).to_not include status2
|
||||
expect(results).to include both
|
||||
|
@ -46,15 +46,23 @@ describe HashtagQueryService, type: :service do
|
|||
|
||||
it 'can restrict to an account' do
|
||||
BlockService.new.call(account, status1.account)
|
||||
results = subject.call(tag1, { none: [tag2.name] }, account)
|
||||
results = described_class.new(tag1, account, none: [tag2.name]).get(20)
|
||||
expect(results).to_not include status1
|
||||
end
|
||||
|
||||
it 'can restrict to local' do
|
||||
status1.account.update(domain: 'example.com')
|
||||
status1.update(local: false, uri: 'example.com/toot')
|
||||
results = subject.call(tag1, { any: [tag2.name] }, nil, true)
|
||||
results = described_class.new(tag1, nil, any: [tag2.name], local: true).get(20)
|
||||
expect(results).to_not include status1
|
||||
end
|
||||
|
||||
it 'allows replies to be included' do
|
||||
original = Fabricate(:status)
|
||||
status = Fabricate(:status, tags: [tag1], in_reply_to_id: original.id)
|
||||
|
||||
results = described_class.new(tag1, nil).get(20)
|
||||
expect(results).to include(status)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -151,6 +151,12 @@ RSpec.describe User, type: :model do
|
|||
expect(user.reload.otp_required_for_login).to be false
|
||||
end
|
||||
|
||||
it 'saves nil for otp_secret' do
|
||||
user = Fabricate.build(:user, otp_secret: 'oldotpcode')
|
||||
user.disable_two_factor!
|
||||
expect(user.reload.otp_secret).to be nil
|
||||
end
|
||||
|
||||
it 'saves cleared otp_backup_codes' do
|
||||
user = Fabricate.build(:user, otp_backup_codes: %w(dummy dummy))
|
||||
user.disable_two_factor!
|
||||
|
|
80
spec/models/webauthn_credentials_spec.rb
Normal file
80
spec/models/webauthn_credentials_spec.rb
Normal file
|
@ -0,0 +1,80 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe WebauthnCredential, type: :model do
|
||||
describe 'validations' do
|
||||
it 'is invalid without an external id' do
|
||||
webauthn_credential = Fabricate.build(:webauthn_credential, external_id: nil)
|
||||
|
||||
webauthn_credential.valid?
|
||||
|
||||
expect(webauthn_credential).to model_have_error_on_field(:external_id)
|
||||
end
|
||||
|
||||
it 'is invalid without a public key' do
|
||||
webauthn_credential = Fabricate.build(:webauthn_credential, public_key: nil)
|
||||
|
||||
webauthn_credential.valid?
|
||||
|
||||
expect(webauthn_credential).to model_have_error_on_field(:public_key)
|
||||
end
|
||||
|
||||
it 'is invalid without a nickname' do
|
||||
webauthn_credential = Fabricate.build(:webauthn_credential, nickname: nil)
|
||||
|
||||
webauthn_credential.valid?
|
||||
|
||||
expect(webauthn_credential).to model_have_error_on_field(:nickname)
|
||||
end
|
||||
|
||||
it 'is invalid without a sign_count' do
|
||||
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: nil)
|
||||
|
||||
webauthn_credential.valid?
|
||||
|
||||
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
|
||||
end
|
||||
|
||||
it 'is invalid if already exist a webauthn credential with the same external id' do
|
||||
existing_webauthn_credential = Fabricate(:webauthn_credential, external_id: "_Typ0ygudDnk9YUVWLQayw")
|
||||
new_webauthn_credential = Fabricate.build(:webauthn_credential, external_id: "_Typ0ygudDnk9YUVWLQayw")
|
||||
|
||||
new_webauthn_credential.valid?
|
||||
|
||||
expect(new_webauthn_credential).to model_have_error_on_field(:external_id)
|
||||
end
|
||||
|
||||
it 'is invalid if user already registered a webauthn credential with the same nickname' do
|
||||
user = Fabricate(:user)
|
||||
existing_webauthn_credential = Fabricate(:webauthn_credential, user_id: user.id, nickname: 'USB Key')
|
||||
new_webauthn_credential = Fabricate.build(:webauthn_credential, user_id: user.id, nickname: 'USB Key')
|
||||
|
||||
new_webauthn_credential.valid?
|
||||
|
||||
expect(new_webauthn_credential).to model_have_error_on_field(:nickname)
|
||||
end
|
||||
|
||||
it 'is invalid if sign_count is not a number' do
|
||||
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 'invalid sign_count')
|
||||
|
||||
webauthn_credential.valid?
|
||||
|
||||
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
|
||||
end
|
||||
|
||||
it 'is invalid if sign_count is negative number' do
|
||||
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: -1)
|
||||
|
||||
webauthn_credential.valid?
|
||||
|
||||
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
|
||||
end
|
||||
|
||||
it 'is invalid if sign_count is greater 2**63 - 1' do
|
||||
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 2**63)
|
||||
|
||||
webauthn_credential.valid?
|
||||
|
||||
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,8 +7,9 @@ RSpec.describe AccountPolicy do
|
|||
let(:subject) { described_class }
|
||||
let(:admin) { Fabricate(:user, admin: true).account }
|
||||
let(:john) { Fabricate(:user).account }
|
||||
let(:alice) { Fabricate(:user).account }
|
||||
|
||||
permissions :index?, :show?, :unsuspend?, :unsilence?, :remove_avatar?, :remove_header? do
|
||||
permissions :index? do
|
||||
context 'staff' do
|
||||
it 'permits' do
|
||||
expect(subject).to permit(admin)
|
||||
|
@ -22,6 +23,38 @@ RSpec.describe AccountPolicy do
|
|||
end
|
||||
end
|
||||
|
||||
permissions :show?, :unsilence?, :unsensitive?, :remove_avatar?, :remove_header? do
|
||||
context 'staff' do
|
||||
it 'permits' do
|
||||
expect(subject).to permit(admin, alice)
|
||||
end
|
||||
end
|
||||
|
||||
context 'not staff' do
|
||||
it 'denies' do
|
||||
expect(subject).to_not permit(john, alice)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
permissions :unsuspend? do
|
||||
before do
|
||||
alice.suspend!
|
||||
end
|
||||
|
||||
context 'staff' do
|
||||
it 'permits' do
|
||||
expect(subject).to permit(admin, alice)
|
||||
end
|
||||
end
|
||||
|
||||
context 'not staff' do
|
||||
it 'denies' do
|
||||
expect(subject).to_not permit(john, alice)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
permissions :redownload?, :subscribe?, :unsubscribe? do
|
||||
context 'admin' do
|
||||
it 'permits' do
|
||||
|
|
|
@ -70,6 +70,8 @@ RSpec::Sidekiq.configure do |config|
|
|||
config.warn_when_jobs_not_processed_by_sidekiq = false
|
||||
end
|
||||
|
||||
RSpec::Matchers.define_negated_matcher :not_change, :change
|
||||
|
||||
def request_fixture(name)
|
||||
File.read(Rails.root.join('spec', 'fixtures', 'requests', name))
|
||||
end
|
||||
|
|
|
@ -73,4 +73,84 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
|
|||
expect(ProofProvider::Keybase::Worker).to have_received(:perform_async)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is not suspended' do
|
||||
let!(:account) { Fabricate(:account, username: 'alice', domain: 'example.com') }
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
id: 'https://foo.test',
|
||||
type: 'Actor',
|
||||
inbox: 'https://foo.test/inbox',
|
||||
suspended: true,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Admin::SuspensionWorker).to receive(:perform_async)
|
||||
end
|
||||
|
||||
subject { described_class.new.call('alice', 'example.com', payload) }
|
||||
|
||||
it 'suspends account remotely' do
|
||||
expect(subject.suspended?).to be true
|
||||
expect(subject.suspension_origin_remote?).to be true
|
||||
end
|
||||
|
||||
it 'queues suspension worker' do
|
||||
subject
|
||||
expect(Admin::SuspensionWorker).to have_received(:perform_async)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is suspended' do
|
||||
let!(:account) { Fabricate(:account, username: 'alice', domain: 'example.com', display_name: '') }
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
id: 'https://foo.test',
|
||||
type: 'Actor',
|
||||
inbox: 'https://foo.test/inbox',
|
||||
suspended: false,
|
||||
name: 'Hoge',
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Admin::UnsuspensionWorker).to receive(:perform_async)
|
||||
|
||||
account.suspend!(origin: suspension_origin)
|
||||
end
|
||||
|
||||
subject { described_class.new.call('alice', 'example.com', payload) }
|
||||
|
||||
context 'locally' do
|
||||
let(:suspension_origin) { :local }
|
||||
|
||||
it 'does not unsuspend it' do
|
||||
expect(subject.suspended?).to be true
|
||||
end
|
||||
|
||||
it 'does not update any attributes' do
|
||||
expect(subject.display_name).to_not eq 'Hoge'
|
||||
end
|
||||
end
|
||||
|
||||
context 'remotely' do
|
||||
let(:suspension_origin) { :remote }
|
||||
|
||||
it 'unsuspends it' do
|
||||
expect(subject.suspended?).to be false
|
||||
end
|
||||
|
||||
it 'queues unsuspension worker' do
|
||||
subject
|
||||
expect(Admin::UnsuspensionWorker).to have_received(:perform_async)
|
||||
end
|
||||
|
||||
it 'updates attributes' do
|
||||
expect(subject.display_name).to eq 'Hoge'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,7 +22,48 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
|
|||
subject { described_class.new }
|
||||
|
||||
describe '#call' do
|
||||
context 'when actor is the sender'
|
||||
context 'when actor is suspended' do
|
||||
before do
|
||||
actor.suspend!(origin: :remote)
|
||||
end
|
||||
|
||||
%w(Accept Add Announce Block Create Flag Follow Like Move Remove).each do |activity_type|
|
||||
context "with #{activity_type} activity" do
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: activity_type,
|
||||
actor: ActivityPub::TagManager.instance.uri_for(actor),
|
||||
}
|
||||
end
|
||||
|
||||
it 'does not process payload' do
|
||||
expect(ActivityPub::Activity).not_to receive(:factory)
|
||||
subject.call(json, actor)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
%w(Delete Reject Undo Update).each do |activity_type|
|
||||
context "with #{activity_type} activity" do
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: activity_type,
|
||||
actor: ActivityPub::TagManager.instance.uri_for(actor),
|
||||
}
|
||||
end
|
||||
|
||||
it 'processes the payload' do
|
||||
expect(ActivityPub::Activity).to receive(:factory)
|
||||
subject.call(json, actor)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when actor differs from sender' do
|
||||
let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') }
|
||||
|
||||
|
|
105
spec/services/activitypub/synchronize_followers_service_spec.rb
Normal file
105
spec/services/activitypub/synchronize_followers_service_spec.rb
Normal file
|
@ -0,0 +1,105 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
|
||||
let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account', inbox_url: 'http://example.com/inbox') }
|
||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||
let(:bob) { Fabricate(:account, username: 'bob') }
|
||||
let(:eve) { Fabricate(:account, username: 'eve') }
|
||||
let(:mallory) { Fabricate(:account, username: 'mallory') }
|
||||
let(:collection_uri) { 'http://example.com/partial-followers' }
|
||||
|
||||
let(:items) do
|
||||
[
|
||||
ActivityPub::TagManager.instance.uri_for(alice),
|
||||
ActivityPub::TagManager.instance.uri_for(eve),
|
||||
ActivityPub::TagManager.instance.uri_for(mallory),
|
||||
]
|
||||
end
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'Collection',
|
||||
id: collection_uri,
|
||||
items: items,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
shared_examples 'synchronizes followers' do
|
||||
before do
|
||||
alice.follow!(actor)
|
||||
bob.follow!(actor)
|
||||
mallory.request_follow!(actor)
|
||||
|
||||
allow(ActivityPub::DeliveryWorker).to receive(:perform_async)
|
||||
|
||||
subject.call(actor, collection_uri)
|
||||
end
|
||||
|
||||
it 'keeps expected followers' do
|
||||
expect(alice.following?(actor)).to be true
|
||||
end
|
||||
|
||||
it 'removes local followers not in the remote list' do
|
||||
expect(bob.following?(actor)).to be false
|
||||
end
|
||||
|
||||
it 'converts follow requests to follow relationships when they have been accepted' do
|
||||
expect(mallory.following?(actor)).to be true
|
||||
end
|
||||
|
||||
it 'sends an Undo Follow to the actor' do
|
||||
expect(ActivityPub::DeliveryWorker).to have_received(:perform_async).with(anything, eve.id, actor.inbox_url)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#call' do
|
||||
context 'when the endpoint is a Collection of actor URIs' do
|
||||
before do
|
||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
||||
end
|
||||
|
||||
it_behaves_like 'synchronizes followers'
|
||||
end
|
||||
|
||||
context 'when the endpoint is an OrderedCollection of actor URIs' do
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'OrderedCollection',
|
||||
id: collection_uri,
|
||||
orderedItems: items,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
before do
|
||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
||||
end
|
||||
|
||||
it_behaves_like 'synchronizes followers'
|
||||
end
|
||||
|
||||
context 'when the endpoint is a paginated Collection of actor URIs' do
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'Collection',
|
||||
id: collection_uri,
|
||||
first: {
|
||||
type: 'CollectionPage',
|
||||
partOf: collection_uri,
|
||||
items: items,
|
||||
}
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
before do
|
||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
||||
end
|
||||
|
||||
it_behaves_like 'synchronizes followers'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,6 +3,7 @@ require 'rails_helper'
|
|||
RSpec.describe AppSignUpService, type: :service do
|
||||
let(:app) { Fabricate(:application, scopes: 'read write') }
|
||||
let(:good_params) { { username: 'alice', password: '12345678', email: 'good@email.com', agreement: true } }
|
||||
let(:remote_ip) { IPAddr.new('198.0.2.1') }
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
|
@ -10,16 +11,16 @@ RSpec.describe AppSignUpService, type: :service do
|
|||
it 'returns nil when registrations are closed' do
|
||||
tmp = Setting.registrations_mode
|
||||
Setting.registrations_mode = 'none'
|
||||
expect(subject.call(app, good_params)).to be_nil
|
||||
expect(subject.call(app, remote_ip, good_params)).to be_nil
|
||||
Setting.registrations_mode = tmp
|
||||
end
|
||||
|
||||
it 'raises an error when params are missing' do
|
||||
expect { subject.call(app, {}) }.to raise_error ActiveRecord::RecordInvalid
|
||||
expect { subject.call(app, remote_ip, {}) }.to raise_error ActiveRecord::RecordInvalid
|
||||
end
|
||||
|
||||
it 'creates an unconfirmed user with access token' do
|
||||
access_token = subject.call(app, good_params)
|
||||
access_token = subject.call(app, remote_ip, good_params)
|
||||
expect(access_token).to_not be_nil
|
||||
user = User.find_by(id: access_token.resource_owner_id)
|
||||
expect(user).to_not be_nil
|
||||
|
@ -27,13 +28,13 @@ RSpec.describe AppSignUpService, type: :service do
|
|||
end
|
||||
|
||||
it 'creates access token with the app\'s scopes' do
|
||||
access_token = subject.call(app, good_params)
|
||||
access_token = subject.call(app, remote_ip, good_params)
|
||||
expect(access_token).to_not be_nil
|
||||
expect(access_token.scopes.to_s).to eq 'read write'
|
||||
end
|
||||
|
||||
it 'creates an account' do
|
||||
access_token = subject.call(app, good_params)
|
||||
access_token = subject.call(app, remote_ip, good_params)
|
||||
expect(access_token).to_not be_nil
|
||||
user = User.find_by(id: access_token.resource_owner_id)
|
||||
expect(user).to_not be_nil
|
||||
|
@ -42,7 +43,7 @@ RSpec.describe AppSignUpService, type: :service do
|
|||
end
|
||||
|
||||
it 'creates an account with invite request text' do
|
||||
access_token = subject.call(app, good_params.merge(reason: 'Foo bar'))
|
||||
access_token = subject.call(app, remote_ip, good_params.merge(reason: 'Foo bar'))
|
||||
expect(access_token).to_not be_nil
|
||||
user = User.find_by(id: access_token.resource_owner_id)
|
||||
expect(user).to_not be_nil
|
||||
|
|
|
@ -26,6 +26,11 @@ RSpec.describe BatchedRemoveStatusService, type: :service do
|
|||
subject.call([status1, status2])
|
||||
end
|
||||
|
||||
it 'removes statuses' do
|
||||
expect { Status.find(status1.id) }.to raise_error ActiveRecord::RecordNotFound
|
||||
expect { Status.find(status2.id) }.to raise_error ActiveRecord::RecordNotFound
|
||||
end
|
||||
|
||||
it 'removes statuses from author\'s home feed' do
|
||||
expect(HomeFeed.new(alice).get(10)).to_not include([status1.id, status2.id])
|
||||
end
|
||||
|
@ -38,10 +43,6 @@ RSpec.describe BatchedRemoveStatusService, type: :service do
|
|||
expect(Redis.current).to have_received(:publish).with("timeline:#{jeff.id}", any_args).at_least(:once)
|
||||
end
|
||||
|
||||
it 'notifies streaming API of author' do
|
||||
expect(Redis.current).to have_received(:publish).with("timeline:#{alice.id}", any_args).at_least(:once)
|
||||
end
|
||||
|
||||
it 'notifies streaming API of public timeline' do
|
||||
expect(Redis.current).to have_received(:publish).with('timeline:public', any_args).at_least(:once)
|
||||
end
|
||||
|
|
96
spec/services/delete_account_service_spec.rb
Normal file
96
spec/services/delete_account_service_spec.rb
Normal file
|
@ -0,0 +1,96 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe DeleteAccountService, type: :service do
|
||||
shared_examples 'common behavior' do
|
||||
let!(:status) { Fabricate(:status, account: account) }
|
||||
let!(:mention) { Fabricate(:mention, account: local_follower) }
|
||||
let!(:status_with_mention) { Fabricate(:status, account: account, mentions: [mention]) }
|
||||
let!(:media_attachment) { Fabricate(:media_attachment, account: account) }
|
||||
let!(:notification) { Fabricate(:notification, account: account) }
|
||||
let!(:favourite) { Fabricate(:favourite, account: account, status: Fabricate(:status, account: local_follower)) }
|
||||
let!(:poll) { Fabricate(:poll, account: account) }
|
||||
let!(:poll_vote) { Fabricate(:poll_vote, account: local_follower, poll: poll) }
|
||||
|
||||
let!(:active_relationship) { Fabricate(:follow, account: account, target_account: local_follower) }
|
||||
let!(:passive_relationship) { Fabricate(:follow, account: local_follower, target_account: account) }
|
||||
let!(:endorsement) { Fabricate(:account_pin, account: local_follower, target_account: account) }
|
||||
|
||||
let!(:mention_notification) { Fabricate(:notification, account: local_follower, activity: mention, type: :mention) }
|
||||
let!(:status_notification) { Fabricate(:notification, account: local_follower, activity: status, type: :status) }
|
||||
let!(:poll_notification) { Fabricate(:notification, account: local_follower, activity: poll, type: :poll) }
|
||||
let!(:favourite_notification) { Fabricate(:notification, account: local_follower, activity: favourite, type: :favourite) }
|
||||
let!(:follow_notification) { Fabricate(:notification, account: local_follower, activity: active_relationship, type: :follow) }
|
||||
|
||||
subject do
|
||||
-> { described_class.new.call(account) }
|
||||
end
|
||||
|
||||
it 'deletes associated owned records' do
|
||||
is_expected.to change {
|
||||
[
|
||||
account.statuses,
|
||||
account.media_attachments,
|
||||
account.notifications,
|
||||
account.favourites,
|
||||
account.active_relationships,
|
||||
account.passive_relationships,
|
||||
account.polls,
|
||||
].map(&:count)
|
||||
}.from([2, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0])
|
||||
end
|
||||
|
||||
it 'deletes associated target records' do
|
||||
is_expected.to change {
|
||||
[
|
||||
AccountPin.where(target_account: account),
|
||||
].map(&:count)
|
||||
}.from([1]).to([0])
|
||||
end
|
||||
|
||||
it 'deletes associated target notifications' do
|
||||
is_expected.to change {
|
||||
[
|
||||
'poll', 'favourite', 'status', 'mention', 'follow'
|
||||
].map { |type| Notification.where(type: type).count }
|
||||
}.from([1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#call on local account' do
|
||||
before do
|
||||
stub_request(:post, "https://alice.com/inbox").to_return(status: 201)
|
||||
stub_request(:post, "https://bob.com/inbox").to_return(status: 201)
|
||||
end
|
||||
|
||||
let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', protocol: :activitypub) }
|
||||
let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
|
||||
|
||||
include_examples 'common behavior' do
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:local_follower) { Fabricate(:account) }
|
||||
|
||||
it 'sends a delete actor activity to all known inboxes' do
|
||||
subject.call
|
||||
expect(a_request(:post, "https://alice.com/inbox")).to have_been_made.once
|
||||
expect(a_request(:post, "https://bob.com/inbox")).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#call on remote account' do
|
||||
before do
|
||||
stub_request(:post, "https://alice.com/inbox").to_return(status: 201)
|
||||
stub_request(:post, "https://bob.com/inbox").to_return(status: 201)
|
||||
end
|
||||
|
||||
include_examples 'common behavior' do
|
||||
let!(:account) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
|
||||
let!(:local_follower) { Fabricate(:account) }
|
||||
|
||||
it 'sends a reject follow to follwer inboxes' do
|
||||
subject.call
|
||||
expect(a_request(:post, account.inbox_url)).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -28,10 +28,10 @@ RSpec.describe FanOutOnWriteService, type: :service do
|
|||
end
|
||||
|
||||
it 'delivers status to hashtag' do
|
||||
expect(Tag.find_by!(name: 'test').statuses.pluck(:id)).to include status.id
|
||||
expect(TagFeed.new(Tag.find_by(name: 'test'), alice).get(20).map(&:id)).to include status.id
|
||||
end
|
||||
|
||||
it 'delivers status to public timeline' do
|
||||
expect(Status.as_public_timeline(alice).map(&:id)).to include status.id
|
||||
expect(PublicFeed.new(alice).get(20).map(&:id)).to include status.id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ImportService, type: :service do
|
||||
include RoutingHelper
|
||||
|
||||
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, protocol: :activitypub, inbox_url: 'https://example.com/inbox') }
|
||||
|
@ -95,6 +97,7 @@ 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 1
|
||||
expect(account.follow_requests.count).to eq 1
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
|
@ -168,4 +171,44 @@ RSpec.describe ImportService, type: :service do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'import bookmarks' do
|
||||
subject { ImportService.new }
|
||||
|
||||
let(:csv) { attachment_fixture('bookmark-imports.txt') }
|
||||
|
||||
around(:each) do |example|
|
||||
local_before = Rails.configuration.x.local_domain
|
||||
web_before = Rails.configuration.x.web_domain
|
||||
Rails.configuration.x.local_domain = 'local.com'
|
||||
Rails.configuration.x.web_domain = 'local.com'
|
||||
example.run
|
||||
Rails.configuration.x.web_domain = web_before
|
||||
Rails.configuration.x.local_domain = local_before
|
||||
end
|
||||
|
||||
let(:local_account) { Fabricate(:account, username: 'foo', domain: '') }
|
||||
let!(:remote_status) { Fabricate(:status, uri: 'https://example.com/statuses/1312') }
|
||||
let!(:direct_status) { Fabricate(:status, uri: 'https://example.com/statuses/direct', visibility: :direct) }
|
||||
|
||||
before do
|
||||
service = double
|
||||
allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service)
|
||||
allow(service).to receive(:call).with('https://unknown-remote.com/users/bar/statuses/1') do
|
||||
Fabricate(:status, uri: 'https://unknown-remote.com/users/bar/statuses/1')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when no bookmarks are set' do
|
||||
let(:import) { Import.create(account: account, type: 'bookmarks', data: csv) }
|
||||
it 'adds the toots the user has access to to bookmarks' do
|
||||
local_status = Fabricate(:status, account: local_account, uri: 'https://local.com/users/foo/statuses/42', id: 42, local: true)
|
||||
subject.call(import)
|
||||
expect(account.bookmarks.map(&:status).map(&:id)).to include(local_status.id)
|
||||
expect(account.bookmarks.map(&:status).map(&:id)).to include(remote_status.id)
|
||||
expect(account.bookmarks.map(&:status).map(&:id)).not_to include(direct_status.id)
|
||||
expect(account.bookmarks.count).to eq 3
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,13 +2,14 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe NotifyService, type: :service do
|
||||
subject do
|
||||
-> { described_class.new.call(recipient, activity) }
|
||||
-> { described_class.new.call(recipient, type, activity) }
|
||||
end
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:recipient) { user.account }
|
||||
let(:sender) { Fabricate(:account, domain: 'example.com') }
|
||||
let(:activity) { Fabricate(:follow, account: sender, target_account: recipient) }
|
||||
let(:type) { :follow }
|
||||
|
||||
it { is_expected.to change(Notification, :count).by(1) }
|
||||
|
||||
|
@ -50,6 +51,7 @@ RSpec.describe NotifyService, type: :service do
|
|||
|
||||
context 'for direct messages' do
|
||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct)) }
|
||||
let(:type) { :mention }
|
||||
|
||||
before do
|
||||
user.settings.interactions = user.settings.interactions.merge('must_be_following_dm' => enabled)
|
||||
|
@ -93,6 +95,7 @@ RSpec.describe NotifyService, type: :service do
|
|||
describe 'reblogs' do
|
||||
let(:status) { Fabricate(:status, account: Fabricate(:account)) }
|
||||
let(:activity) { Fabricate(:status, account: sender, reblog: status) }
|
||||
let(:type) { :reblog }
|
||||
|
||||
it 'shows reblogs by default' do
|
||||
recipient.follow!(sender)
|
||||
|
@ -114,6 +117,7 @@ RSpec.describe NotifyService, type: :service do
|
|||
let(:asshole) { Fabricate(:account, username: 'asshole') }
|
||||
let(:reply_to) { Fabricate(:status, account: asshole) }
|
||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, thread: reply_to)) }
|
||||
let(:type) { :mention }
|
||||
|
||||
it 'does not notify when conversation is muted' do
|
||||
recipient.mute_conversation!(activity.status.conversation)
|
||||
|
|
|
@ -3,7 +3,7 @@ require 'rails_helper'
|
|||
RSpec.describe RemoveStatusService, type: :service do
|
||||
subject { RemoveStatusService.new }
|
||||
|
||||
let!(:alice) { Fabricate(:account) }
|
||||
let!(:alice) { Fabricate(:account, user: Fabricate(:user)) }
|
||||
let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') }
|
||||
let!(:jeff) { Fabricate(:account) }
|
||||
let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
|
@ -17,23 +17,33 @@ RSpec.describe RemoveStatusService, type: :service do
|
|||
hank.follow!(alice)
|
||||
|
||||
@status = PostStatusService.new.call(alice, text: 'Hello @bob@example.com')
|
||||
FavouriteService.new.call(jeff, @status)
|
||||
Fabricate(:status, account: bill, reblog: @status, uri: 'hoge')
|
||||
subject.call(@status)
|
||||
end
|
||||
|
||||
it 'removes status from author\'s home feed' do
|
||||
subject.call(@status)
|
||||
expect(HomeFeed.new(alice).get(10)).to_not include(@status.id)
|
||||
end
|
||||
|
||||
it 'removes status from local follower\'s home feed' do
|
||||
subject.call(@status)
|
||||
expect(HomeFeed.new(jeff).get(10)).to_not include(@status.id)
|
||||
end
|
||||
|
||||
it 'sends delete activity to followers' do
|
||||
subject.call(@status)
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice
|
||||
end
|
||||
|
||||
it 'sends delete activity to rebloggers' do
|
||||
subject.call(@status)
|
||||
expect(a_request(:post, 'http://example2.com/inbox')).to have_been_made
|
||||
end
|
||||
|
||||
it 'remove status from notifications' do
|
||||
expect { subject.call(@status) }.to change {
|
||||
Notification.where(activity_type: 'Favourite', from_account: jeff, account: alice).count
|
||||
}.from(1).to(0)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,23 +4,101 @@ RSpec.describe ResolveAccountService, type: :service do
|
|||
subject { described_class.new }
|
||||
|
||||
before do
|
||||
stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
|
||||
stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:catsrgr8@example.com").to_return(status: 404)
|
||||
stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404)
|
||||
stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.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)
|
||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:hoge@example.com').to_return(status: 410)
|
||||
end
|
||||
|
||||
it 'raises error if no such user can be resolved via webfinger' do
|
||||
expect(subject.call('catsrgr8@quitter.no')).to be_nil
|
||||
context 'when there is an LRDD endpoint but no resolvable account' 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:catsrgr8@quitter.no").to_return(status: 404)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.call('catsrgr8@quitter.no')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it 'raises error if the domain does not have webfinger' do
|
||||
expect(subject.call('catsrgr8@example.com')).to be_nil
|
||||
context 'when there is no LRDD endpoint nor resolvable account' do
|
||||
before do
|
||||
stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:catsrgr8@example.com").to_return(status: 404)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.call('catsrgr8@example.com')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when webfinger returns http gone' do
|
||||
context 'for a previously known account' do
|
||||
before do
|
||||
Fabricate(:account, username: 'hoge', domain: 'example.com', last_webfingered_at: nil)
|
||||
allow(AccountDeletionWorker).to receive(:perform_async)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.call('hoge@example.com')).to be_nil
|
||||
end
|
||||
|
||||
it 'queues account deletion worker' do
|
||||
subject.call('hoge@example.com')
|
||||
expect(AccountDeletionWorker).to have_received(:perform_async)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a previously unknown account' do
|
||||
it 'returns nil' do
|
||||
expect(subject.call('hoge@example.com')).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a legitimate webfinger redirection' do
|
||||
before do
|
||||
webfinger = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] }
|
||||
stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
it 'returns new remote account' do
|
||||
account = subject.call('Foo@redirected.example.com')
|
||||
|
||||
expect(account.activitypub?).to eq true
|
||||
expect(account.acct).to eq 'foo@ap.example.com'
|
||||
expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a misconfigured redirection' do
|
||||
before do
|
||||
webfinger = { subject: 'acct:Foo@redirected.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] }
|
||||
stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
it 'returns new remote account' do
|
||||
account = subject.call('Foo@redirected.example.com')
|
||||
|
||||
expect(account.activitypub?).to eq true
|
||||
expect(account.acct).to eq 'foo@ap.example.com'
|
||||
expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with too many webfinger redirections' do
|
||||
before do
|
||||
webfinger = { subject: 'acct:foo@evil.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] }
|
||||
stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
webfinger2 = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] }
|
||||
stub_request(:get, 'https://evil.example.com/.well-known/webfinger?resource=acct:foo@evil.example.com').to_return(body: Oj.dump(webfinger2), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
it 'returns new remote account' do
|
||||
expect { subject.call('Foo@redirected.example.com') }.to raise_error Webfinger::RedirectError
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an ActivityPub account' do
|
||||
|
@ -48,6 +126,41 @@ RSpec.describe ResolveAccountService, type: :service do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with an already-known actor changing acct: URI' do
|
||||
let!(:duplicate) { Fabricate(:account, username: 'foo', domain: 'old.example.com', uri: 'https://ap.example.com/users/foo') }
|
||||
let!(:status) { Fabricate(:status, account: duplicate, text: 'foo') }
|
||||
|
||||
it 'returns new remote account' do
|
||||
account = subject.call('foo@ap.example.com')
|
||||
|
||||
expect(account.activitypub?).to eq true
|
||||
expect(account.domain).to eq 'ap.example.com'
|
||||
expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
|
||||
expect(account.uri).to eq 'https://ap.example.com/users/foo'
|
||||
end
|
||||
|
||||
it 'merges accounts' do
|
||||
account = subject.call('foo@ap.example.com')
|
||||
|
||||
expect(status.reload.account_id).to eq account.id
|
||||
expect(Account.where(uri: account.uri).count).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an already-known acct: URI changing ActivityPub id' do
|
||||
let!(:old_account) { Fabricate(:account, username: 'foo', domain: 'ap.example.com', uri: 'https://old.example.com/users/foo', last_webfingered_at: nil) }
|
||||
let!(:status) { Fabricate(:status, account: old_account, text: 'foo') }
|
||||
|
||||
it 'returns new remote account' do
|
||||
account = subject.call('foo@ap.example.com')
|
||||
|
||||
expect(account.activitypub?).to eq true
|
||||
expect(account.domain).to eq 'ap.example.com'
|
||||
expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
|
||||
expect(account.uri).to eq 'https://ap.example.com/users/foo'
|
||||
end
|
||||
end
|
||||
|
||||
it 'processes one remote account at a time using locks' do
|
||||
wait_for_start = true
|
||||
fail_occurred = false
|
||||
|
|
|
@ -15,5 +15,102 @@ describe ResolveURLService, type: :service do
|
|||
|
||||
expect(subject.call(url)).to be_nil
|
||||
end
|
||||
|
||||
context 'searching for a remote private status' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:poster) { Fabricate(:account, domain: 'example.com') }
|
||||
let(:url) { 'https://example.com/@foo/42' }
|
||||
let(:uri) { 'https://example.com/users/foo/statuses/42' }
|
||||
let!(:status) { Fabricate(:status, url: url, uri: uri, account: poster, visibility: :private) }
|
||||
|
||||
before do
|
||||
stub_request(:get, url).to_return(status: 404) if url.present?
|
||||
stub_request(:get, uri).to_return(status: 404)
|
||||
end
|
||||
|
||||
context 'when the account follows the poster' do
|
||||
before do
|
||||
account.follow!(poster)
|
||||
end
|
||||
|
||||
context 'when the status uses Mastodon-style URLs' do
|
||||
let(:url) { 'https://example.com/@foo/42' }
|
||||
let(:uri) { 'https://example.com/users/foo/statuses/42' }
|
||||
|
||||
it 'returns status by url' do
|
||||
expect(subject.call(url, on_behalf_of: account)).to eq(status)
|
||||
end
|
||||
|
||||
it 'returns status by uri' do
|
||||
expect(subject.call(uri, on_behalf_of: account)).to eq(status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status uses pleroma-style URLs' do
|
||||
let(:url) { nil }
|
||||
let(:uri) { 'https://example.com/objects/0123-456-789-abc-def' }
|
||||
|
||||
it 'returns status by uri' do
|
||||
expect(subject.call(uri, on_behalf_of: account)).to eq(status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account does not follow the poster' do
|
||||
context 'when the status uses Mastodon-style URLs' do
|
||||
let(:url) { 'https://example.com/@foo/42' }
|
||||
let(:uri) { 'https://example.com/users/foo/statuses/42' }
|
||||
|
||||
it 'does not return the status by url' do
|
||||
expect(subject.call(url, on_behalf_of: account)).to be_nil
|
||||
end
|
||||
|
||||
it 'does not return the status by uri' do
|
||||
expect(subject.call(uri, on_behalf_of: account)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status uses pleroma-style URLs' do
|
||||
let(:url) { nil }
|
||||
let(:uri) { 'https://example.com/objects/0123-456-789-abc-def' }
|
||||
|
||||
it 'returns status by uri' do
|
||||
expect(subject.call(uri, on_behalf_of: account)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'searching for a local private status' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:poster) { Fabricate(:account) }
|
||||
let!(:status) { Fabricate(:status, account: poster, visibility: :private) }
|
||||
let(:url) { ActivityPub::TagManager.instance.url_for(status) }
|
||||
let(:uri) { ActivityPub::TagManager.instance.uri_for(status) }
|
||||
|
||||
context 'when the account follows the poster' do
|
||||
before do
|
||||
account.follow!(poster)
|
||||
end
|
||||
|
||||
it 'returns status by url' do
|
||||
expect(subject.call(url, on_behalf_of: account)).to eq(status)
|
||||
end
|
||||
|
||||
it 'returns status by uri' do
|
||||
expect(subject.call(uri, on_behalf_of: account)).to eq(status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account does not follow the poster' do
|
||||
it 'does not return the status by url' do
|
||||
expect(subject.call(url, on_behalf_of: account)).to be_nil
|
||||
end
|
||||
|
||||
it 'does not return the status by uri' do
|
||||
expect(subject.call(uri, on_behalf_of: account)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SuspendAccountService, type: :service do
|
||||
describe '#call on local account' do
|
||||
before do
|
||||
stub_request(:post, "https://alice.com/inbox").to_return(status: 201)
|
||||
stub_request(:post, "https://bob.com/inbox").to_return(status: 201)
|
||||
end
|
||||
|
||||
subject do
|
||||
-> { described_class.new.call(account) }
|
||||
end
|
||||
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:status) { Fabricate(:status, account: account) }
|
||||
let!(:media_attachment) { Fabricate(:media_attachment, account: account) }
|
||||
let!(:notification) { Fabricate(:notification, account: account) }
|
||||
let!(:favourite) { Fabricate(:favourite, account: account) }
|
||||
let!(:active_relationship) { Fabricate(:follow, account: account) }
|
||||
let!(:passive_relationship) { Fabricate(:follow, target_account: account) }
|
||||
let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', protocol: :activitypub) }
|
||||
let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
|
||||
let!(:endorsment) { Fabricate(:account_pin, account: passive_relationship.account, target_account: account) }
|
||||
|
||||
it 'deletes associated records' do
|
||||
is_expected.to change {
|
||||
[
|
||||
account.statuses,
|
||||
account.media_attachments,
|
||||
account.notifications,
|
||||
account.favourites,
|
||||
account.active_relationships,
|
||||
account.passive_relationships,
|
||||
AccountPin.where(target_account: account),
|
||||
].map(&:count)
|
||||
}.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
|
||||
subject.call
|
||||
expect(a_request(:post, "https://alice.com/inbox")).to have_been_made.once
|
||||
expect(a_request(:post, "https://bob.com/inbox")).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe '#call on remote account' do
|
||||
before do
|
||||
stub_request(:post, "https://alice.com/inbox").to_return(status: 201)
|
||||
stub_request(:post, "https://bob.com/inbox").to_return(status: 201)
|
||||
end
|
||||
|
||||
subject do
|
||||
-> { described_class.new.call(remote_bob) }
|
||||
end
|
||||
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', protocol: :activitypub) }
|
||||
let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
|
||||
let!(:status) { Fabricate(:status, account: remote_bob) }
|
||||
let!(:media_attachment) { Fabricate(:media_attachment, account: remote_bob) }
|
||||
let!(:notification) { Fabricate(:notification, account: remote_bob) }
|
||||
let!(:favourite) { Fabricate(:favourite, account: remote_bob) }
|
||||
let!(:active_relationship) { Fabricate(:follow, account: remote_bob, target_account: account) }
|
||||
let!(:passive_relationship) { Fabricate(:follow, target_account: remote_bob) }
|
||||
|
||||
it 'deletes associated records' do
|
||||
is_expected.to change {
|
||||
[
|
||||
remote_bob.statuses,
|
||||
remote_bob.media_attachments,
|
||||
remote_bob.notifications,
|
||||
remote_bob.favourites,
|
||||
remote_bob.active_relationships,
|
||||
remote_bob.passive_relationships,
|
||||
].map(&:count)
|
||||
}.from([1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0])
|
||||
end
|
||||
|
||||
it 'sends a reject follow to follwer inboxes' do
|
||||
subject.call
|
||||
expect(a_request(:post, remote_bob.inbox_url)).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
|
@ -55,9 +55,9 @@ RSpec.describe UnallowDomainService, type: :service do
|
|||
end
|
||||
|
||||
it 'removes the remote accounts\'s statuses and media attachments' do
|
||||
expect { bad_status1.reload }.to_not raise_exception ActiveRecord::RecordNotFound
|
||||
expect { bad_status2.reload }.to_not raise_exception ActiveRecord::RecordNotFound
|
||||
expect { bad_attachment.reload }.to_not raise_exception ActiveRecord::RecordNotFound
|
||||
expect { bad_status1.reload }.to_not raise_error
|
||||
expect { bad_status2.reload }.to_not raise_error
|
||||
expect { bad_attachment.reload }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
|
|||
let(:blocked_email) { true }
|
||||
|
||||
it 'calls errors.add' do
|
||||
expect(errors).to have_received(:add).with(:email, I18n.t('users.invalid_email'))
|
||||
expect(errors).to have_received(:add).with(:email, I18n.t('users.blocked_email_provider'))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -25,7 +25,7 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
|
|||
let(:blocked_email) { false }
|
||||
|
||||
it 'not calls errors.add' do
|
||||
expect(errors).not_to have_received(:add).with(:email, I18n.t('users.invalid_email'))
|
||||
expect(errors).not_to have_received(:add).with(:email, I18n.t('users.blocked_email_provider'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,16 +3,22 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ActivityPub::DeliveryWorker do
|
||||
include RoutingHelper
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:payload) { 'test' }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Account).to receive(:remote_followers_hash).with('https://example.com/').and_return('somehash')
|
||||
end
|
||||
|
||||
describe 'perform' do
|
||||
it 'performs a request' do
|
||||
stub_request(:post, 'https://example.com/api').to_return(status: 200)
|
||||
subject.perform(payload, sender.id, 'https://example.com/api')
|
||||
expect(a_request(:post, 'https://example.com/api')).to have_been_made.once
|
||||
subject.perform(payload, sender.id, 'https://example.com/api', { synchronize_followers: true })
|
||||
expect(a_request(:post, 'https://example.com/api').with(headers: { 'Collection-Synchronization' => "collectionId=\"#{account_followers_url(sender)}\", digest=\"somehash\", url=\"#{account_followers_synchronization_url(sender)}\"" })).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'raises when request fails' do
|
||||
|
|
|
@ -23,8 +23,8 @@ describe RefollowWorker do
|
|||
result = subject.perform(account.id)
|
||||
|
||||
expect(result).to be_nil
|
||||
expect(service).to have_received(:call).with(alice, account, reblogs: true)
|
||||
expect(service).to have_received(:call).with(bob, account, reblogs: false)
|
||||
expect(service).to have_received(:call).with(alice, account, reblogs: true, notify: false, bypass_limit: true)
|
||||
expect(service).to have_received(:call).with(bob, account, reblogs: false, notify: false, bypass_limit: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,8 +16,8 @@ describe Scheduler::FeedCleanupScheduler do
|
|||
|
||||
expect(Redis.current.zcard(feed_key_for(inactive_user))).to eq 0
|
||||
expect(Redis.current.zcard(feed_key_for(active_user))).to eq 1
|
||||
expect(Redis.current.exists(feed_key_for(inactive_user, 'reblogs'))).to be false
|
||||
expect(Redis.current.exists(feed_key_for(inactive_user, 'reblogs:2'))).to be false
|
||||
expect(Redis.current.exists?(feed_key_for(inactive_user, 'reblogs'))).to be false
|
||||
expect(Redis.current.exists?(feed_key_for(inactive_user, 'reblogs:2'))).to be false
|
||||
end
|
||||
|
||||
def feed_key_for(user, subtype = nil)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue