Merge remote-tracking branch 'parent/main' into upstream-20231225
This commit is contained in:
commit
4355dfc64f
151 changed files with 1711 additions and 644 deletions
|
@ -10,11 +10,11 @@ describe Api::V1::Accounts::StatusesController do
|
|||
|
||||
before do
|
||||
allow(controller).to receive(:doorkeeper_token) { token }
|
||||
Fabricate(:status, account: user.account)
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
it 'returns expected headers', :aggregate_failures do
|
||||
Fabricate(:status, account: user.account)
|
||||
get :index, params: { account_id: user.account.id, limit: 1 }
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
|
@ -30,7 +30,6 @@ describe Api::V1::Accounts::StatusesController do
|
|||
end
|
||||
|
||||
context 'with exclude replies' do
|
||||
let!(:older_statuses) { user.account.statuses.destroy_all }
|
||||
let!(:status) { Fabricate(:status, account: user.account) }
|
||||
let!(:status_self_reply) { Fabricate(:status, account: user.account, thread: status) }
|
||||
|
||||
|
|
|
@ -15,10 +15,15 @@ RSpec.describe Api::V1::FiltersController do
|
|||
describe 'GET #index' do
|
||||
let(:scopes) { 'read:filters' }
|
||||
let!(:filter) { Fabricate(:custom_filter, account: user.account) }
|
||||
let!(:custom_filter_keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) }
|
||||
|
||||
it 'returns http success' do
|
||||
get :index
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json)
|
||||
.to contain_exactly(
|
||||
include(id: custom_filter_keyword.id.to_s)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -34,28 +34,56 @@ RSpec.describe Api::V2::Admin::AccountsController do
|
|||
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
|
||||
it_behaves_like 'forbidden for wrong role', ''
|
||||
|
||||
[
|
||||
[{ status: 'active', origin: 'local', permissions: 'staff' }, [:admin_account]],
|
||||
[{ by_domain: 'example.org', origin: 'remote' }, [:remote_account]],
|
||||
[{ status: 'suspended' }, [:suspended_remote, :suspended_account]],
|
||||
[{ status: 'disabled' }, [:disabled_account]],
|
||||
[{ status: 'pending' }, [:pending_account]],
|
||||
].each do |params, expected_results|
|
||||
context "when called with #{params.inspect}" do
|
||||
let(:params) { params }
|
||||
context 'when called with status active and origin local and permissions staff' do
|
||||
let(:params) { { status: 'active', origin: 'local', permissions: 'staff' } }
|
||||
|
||||
it "returns the correct accounts (#{expected_results.inspect})" do
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
expect(body_json_ids).to eq(expected_results.map { |symbol| send(symbol).id })
|
||||
end
|
||||
|
||||
def body_json_ids
|
||||
body_as_json.map { |a| a[:id].to_i }
|
||||
end
|
||||
it 'returns the correct accounts' do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_json_ids).to eq([admin_account.id])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when called with by_domain value and origin remote' do
|
||||
let(:params) { { by_domain: 'example.org', origin: 'remote' } }
|
||||
|
||||
it 'returns the correct accounts' do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_json_ids).to include(remote_account.id)
|
||||
expect(body_json_ids).to_not include(other_remote_account.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when called with status suspended' do
|
||||
let(:params) { { status: 'suspended' } }
|
||||
|
||||
it 'returns the correct accounts' do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_json_ids).to include(suspended_remote.id, suspended_account.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when called with status disabled' do
|
||||
let(:params) { { status: 'disabled' } }
|
||||
|
||||
it 'returns the correct accounts' do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_json_ids).to include(disabled_account.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when called with status pending' do
|
||||
let(:params) { { status: 'pending' } }
|
||||
|
||||
it 'returns the correct accounts' do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_json_ids).to include(pending_account.id)
|
||||
end
|
||||
end
|
||||
|
||||
def body_json_ids
|
||||
body_as_json.map { |a| a[:id].to_i }
|
||||
end
|
||||
|
||||
context 'with limit param' do
|
||||
let(:params) { { limit: 1 } }
|
||||
|
||||
|
|
|
@ -22,6 +22,10 @@ RSpec.describe Api::V2::Filters::KeywordsController do
|
|||
it 'returns http success' do
|
||||
get :index, params: { filter_id: filter.id }
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json)
|
||||
.to contain_exactly(
|
||||
include(id: keyword.id.to_s)
|
||||
)
|
||||
end
|
||||
|
||||
context "when trying to access another's user filters" do
|
||||
|
|
|
@ -22,6 +22,10 @@ RSpec.describe Api::V2::Filters::StatusesController do
|
|||
it 'returns http success' do
|
||||
get :index, params: { filter_id: filter.id }
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json)
|
||||
.to contain_exactly(
|
||||
include(id: status_filter.id.to_s)
|
||||
)
|
||||
end
|
||||
|
||||
context "when trying to access another's user filters" do
|
||||
|
|
|
@ -41,8 +41,9 @@ describe Auth::ConfirmationsController do
|
|||
get :show, params: { confirmation_token: 'foobar' }
|
||||
end
|
||||
|
||||
it 'redirects to login' do
|
||||
it 'redirects to login and confirms user' do
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
expect(user.reload.confirmed_at).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -87,8 +88,9 @@ describe Auth::ConfirmationsController do
|
|||
get :show, params: { confirmation_token: 'foobar' }
|
||||
end
|
||||
|
||||
it 'redirects to login' do
|
||||
it 'redirects to login and confirms email' do
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
expect(user.reload.unconfirmed_email).to be_nil
|
||||
end
|
||||
|
||||
it 'does not queue up bootstrapping of home timeline' do
|
||||
|
|
|
@ -70,6 +70,7 @@ describe Auth::PasswordsController do
|
|||
|
||||
it 'deactivates all sessions' do
|
||||
expect(user.session_activations.count).to eq 0
|
||||
expect { session_activation.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'revokes all access tokens' do
|
||||
|
@ -78,6 +79,7 @@ describe Auth::PasswordsController do
|
|||
|
||||
it 'removes push subscriptions' do
|
||||
expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0
|
||||
expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -123,9 +123,8 @@ RSpec.describe Auth::SessionsController do
|
|||
let(:previous_ip) { '1.2.3.4' }
|
||||
let(:current_ip) { '4.3.2.1' }
|
||||
|
||||
let!(:previous_login) { Fabricate(:login_activity, user: user, ip: previous_ip) }
|
||||
|
||||
before do
|
||||
Fabricate(:login_activity, user: user, ip: previous_ip)
|
||||
allow(controller.request).to receive(:remote_ip).and_return(current_ip)
|
||||
user.update(current_sign_in_at: 1.month.ago)
|
||||
post :create, params: { user: { email: user.email, password: user.password } }
|
||||
|
@ -328,12 +327,6 @@ RSpec.describe Auth::SessionsController 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
|
||||
|
||||
let!(:webauthn_credential) do
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
|
||||
|
@ -356,6 +349,11 @@ RSpec.describe Auth::SessionsController do
|
|||
|
||||
let(:fake_credential) { fake_client.get(challenge: challenge, sign_count: sign_count) }
|
||||
|
||||
before do
|
||||
user.generate_otp_backup_codes!
|
||||
user.save
|
||||
end
|
||||
|
||||
context 'when using email and password' do
|
||||
before do
|
||||
post :create, params: { user: { email: user.email, password: user.password } }
|
||||
|
|
|
@ -1,305 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe SignatureVerification do
|
||||
let(:wrapped_actor_class) do
|
||||
Class.new do
|
||||
attr_reader :wrapped_account
|
||||
|
||||
def initialize(wrapped_account)
|
||||
@wrapped_account = wrapped_account
|
||||
end
|
||||
|
||||
delegate :uri, :keypair, to: :wrapped_account
|
||||
end
|
||||
end
|
||||
|
||||
controller(ApplicationController) do
|
||||
include SignatureVerification
|
||||
|
||||
before_action :require_actor_signature!, only: [:signature_required]
|
||||
|
||||
def success
|
||||
head 200
|
||||
end
|
||||
|
||||
def alternative_success
|
||||
head 200
|
||||
end
|
||||
|
||||
def signature_required
|
||||
head 200
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
routes.draw do
|
||||
match :via => [:get, :post], 'success' => 'anonymous#success'
|
||||
match :via => [:get, :post], 'signature_required' => 'anonymous#signature_required'
|
||||
end
|
||||
end
|
||||
|
||||
context 'without signature header' do
|
||||
before do
|
||||
get :success
|
||||
end
|
||||
|
||||
describe '#signed_request?' do
|
||||
it 'returns false' do
|
||||
expect(controller.signed_request?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_request_account' do
|
||||
it 'returns nil' do
|
||||
expect(controller.signed_request_account).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with signature header' do
|
||||
let!(:author) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor') }
|
||||
|
||||
context 'without body' do
|
||||
before do
|
||||
get :success
|
||||
|
||||
fake_request = Request.new(:get, request.url)
|
||||
fake_request.on_behalf_of(author)
|
||||
|
||||
request.headers.merge!(fake_request.headers)
|
||||
end
|
||||
|
||||
describe '#signed_request?' do
|
||||
it 'returns true' do
|
||||
expect(controller.signed_request?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_request_account' do
|
||||
it 'returns an account' do
|
||||
expect(controller.signed_request_account).to eq author
|
||||
end
|
||||
|
||||
it 'returns nil when path does not match' do
|
||||
request.path = '/alternative-path'
|
||||
expect(controller.signed_request_account).to be_nil
|
||||
end
|
||||
|
||||
it 'returns nil when method does not match' do
|
||||
post :success
|
||||
expect(controller.signed_request_account).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a valid actor that is not an Account' do
|
||||
let(:actor) { wrapped_actor_class.new(author) }
|
||||
|
||||
before do
|
||||
get :success
|
||||
|
||||
fake_request = Request.new(:get, request.url)
|
||||
fake_request.on_behalf_of(author)
|
||||
|
||||
request.headers.merge!(fake_request.headers)
|
||||
|
||||
allow(ActivityPub::TagManager.instance).to receive(:uri_to_actor).with(anything) do
|
||||
actor
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_request?' do
|
||||
it 'returns true' do
|
||||
expect(controller.signed_request?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_request_account' do
|
||||
it 'returns nil' do
|
||||
expect(controller.signed_request_account).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_request_actor' do
|
||||
it 'returns the expected actor' do
|
||||
expect(controller.signed_request_actor).to eq actor
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with request with unparsable Date header' do
|
||||
before do
|
||||
get :success
|
||||
|
||||
fake_request = Request.new(:get, request.url)
|
||||
fake_request.add_headers({ 'Date' => 'wrong date' })
|
||||
fake_request.on_behalf_of(author)
|
||||
|
||||
request.headers.merge!(fake_request.headers)
|
||||
end
|
||||
|
||||
describe '#signed_request?' do
|
||||
it 'returns true' do
|
||||
expect(controller.signed_request?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_request_account' do
|
||||
it 'returns nil' do
|
||||
expect(controller.signed_request_account).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signature_verification_failure_reason' do
|
||||
it 'contains an error description' do
|
||||
controller.signed_request_account
|
||||
expect(controller.signature_verification_failure_reason[:error]).to eq 'Invalid Date header: not RFC 2616 compliant date: "wrong date"'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with request older than a day' do
|
||||
before do
|
||||
get :success
|
||||
|
||||
fake_request = Request.new(:get, request.url)
|
||||
fake_request.add_headers({ 'Date' => 2.days.ago.utc.httpdate })
|
||||
fake_request.on_behalf_of(author)
|
||||
|
||||
request.headers.merge!(fake_request.headers)
|
||||
end
|
||||
|
||||
describe '#signed_request?' do
|
||||
it 'returns true' do
|
||||
expect(controller.signed_request?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_request_account' do
|
||||
it 'returns nil' do
|
||||
expect(controller.signed_request_account).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signature_verification_failure_reason' do
|
||||
it 'contains an error description' do
|
||||
controller.signed_request_account
|
||||
expect(controller.signature_verification_failure_reason[:error]).to eq 'Signed request date outside acceptable time window'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with inaccessible key' do
|
||||
before do
|
||||
get :success
|
||||
|
||||
author = Fabricate(:account, domain: 'localhost:5000', uri: 'http://localhost:5000/actor')
|
||||
fake_request = Request.new(:get, request.url)
|
||||
fake_request.on_behalf_of(author)
|
||||
author.destroy
|
||||
|
||||
request.headers.merge!(fake_request.headers)
|
||||
|
||||
stub_request(:get, 'http://localhost:5000/actor#main-key').to_raise(Mastodon::HostValidationError)
|
||||
end
|
||||
|
||||
describe '#signed_request?' do
|
||||
it 'returns true' do
|
||||
expect(controller.signed_request?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_request_account' do
|
||||
it 'returns nil' do
|
||||
expect(controller.signed_request_account).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with body' do
|
||||
before do
|
||||
allow(controller).to receive(:actor_refresh_key!).and_return(author)
|
||||
post :success, body: 'Hello world'
|
||||
|
||||
fake_request = Request.new(:post, request.url, body: 'Hello world')
|
||||
fake_request.on_behalf_of(author)
|
||||
|
||||
request.headers.merge!(fake_request.headers)
|
||||
end
|
||||
|
||||
describe '#signed_request?' do
|
||||
it 'returns true' do
|
||||
expect(controller.signed_request?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signed_request_account' do
|
||||
it 'returns an account' do
|
||||
expect(controller.signed_request_account).to eq author
|
||||
end
|
||||
end
|
||||
|
||||
context 'when path does not match' do
|
||||
before do
|
||||
request.path = '/alternative-path'
|
||||
end
|
||||
|
||||
describe '#signed_request_account' do
|
||||
it 'returns nil' do
|
||||
expect(controller.signed_request_account).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signature_verification_failure_reason' do
|
||||
it 'contains an error description' do
|
||||
controller.signed_request_account
|
||||
expect(controller.signature_verification_failure_reason[:error]).to include('using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)')
|
||||
expect(controller.signature_verification_failure_reason[:signed_string]).to include("(request-target): post /alternative-path\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when method does not match' do
|
||||
before do
|
||||
get :success
|
||||
end
|
||||
|
||||
describe '#signed_request_account' do
|
||||
it 'returns nil' do
|
||||
expect(controller.signed_request_account).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when body has been tampered' do
|
||||
before do
|
||||
post :success, body: 'doo doo doo'
|
||||
end
|
||||
|
||||
describe '#signed_request_account' do
|
||||
it 'returns nil when body has been tampered' do
|
||||
expect(controller.signed_request_account).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a signature is required' do
|
||||
before do
|
||||
get :signature_required
|
||||
end
|
||||
|
||||
context 'without signature header' do
|
||||
it 'returns HTTP 401' do
|
||||
expect(response).to have_http_status(401)
|
||||
end
|
||||
|
||||
it 'returns an error' do
|
||||
expect(Oj.load(response.body)['error']).to eq 'Request not signed'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -48,6 +48,13 @@ describe FollowerAccountsController do
|
|||
|
||||
it 'returns followers' do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json)
|
||||
.to include(
|
||||
orderedItems: contain_exactly(
|
||||
include(follow_from_bob.account.username),
|
||||
include(follow_from_chris.account.username)
|
||||
)
|
||||
)
|
||||
expect(body['totalItems']).to eq 2
|
||||
expect(body['partOf']).to be_present
|
||||
end
|
||||
|
|
|
@ -48,6 +48,13 @@ describe FollowingAccountsController do
|
|||
|
||||
it 'returns followers' do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json)
|
||||
.to include(
|
||||
orderedItems: contain_exactly(
|
||||
include(follow_of_bob.target_account.username),
|
||||
include(follow_of_chris.target_account.username)
|
||||
)
|
||||
)
|
||||
expect(body['totalItems']).to eq 2
|
||||
expect(body['partOf']).to be_present
|
||||
end
|
||||
|
|
|
@ -63,5 +63,9 @@ describe Oauth::AuthorizedApplicationsController do
|
|||
it 'removes subscriptions for the application\'s access tokens' do
|
||||
expect(Web::PushSubscription.where(user: user).count).to eq 0
|
||||
end
|
||||
|
||||
it 'removes the web_push_subscription' do
|
||||
expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,5 +20,9 @@ RSpec.describe Oauth::TokensController do
|
|||
it 'removes web push subscription for token' do
|
||||
expect(Web::PushSubscription.where(access_token: access_token).count).to eq 0
|
||||
end
|
||||
|
||||
it 'removes the web_push_subscription' do
|
||||
expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,6 +22,7 @@ RSpec.describe Settings::ImportsController do
|
|||
it 'assigns the expected imports', :aggregate_failures do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(assigns(:recent_imports)).to eq [import]
|
||||
expect(assigns(:recent_imports)).to_not include(other_import)
|
||||
expect(response.headers['Cache-Control']).to include('private, no-store')
|
||||
end
|
||||
end
|
||||
|
@ -138,6 +139,7 @@ RSpec.describe Settings::ImportsController do
|
|||
let(:bulk_import) { Fabricate(:bulk_import, account: user.account, type: import_type, state: :finished) }
|
||||
|
||||
before do
|
||||
rows.each { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
|
||||
bulk_import.update(total_items: bulk_import.rows.count, processed_items: bulk_import.rows.count, imported_items: 0)
|
||||
end
|
||||
|
||||
|
@ -152,11 +154,11 @@ RSpec.describe Settings::ImportsController do
|
|||
context 'with follows' do
|
||||
let(:import_type) { 'following' }
|
||||
|
||||
let!(:rows) do
|
||||
let(:rows) do
|
||||
[
|
||||
{ 'acct' => 'foo@bar' },
|
||||
{ 'acct' => 'user@bar', 'show_reblogs' => false, 'notify' => true, 'languages' => %w(fr de) },
|
||||
].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
|
||||
]
|
||||
end
|
||||
|
||||
include_examples 'export failed rows', "Account address,Show boosts,Notify on new posts,Languages\nfoo@bar,true,false,\nuser@bar,false,true,\"fr, de\"\n"
|
||||
|
@ -165,11 +167,11 @@ RSpec.describe Settings::ImportsController do
|
|||
context 'with blocks' do
|
||||
let(:import_type) { 'blocking' }
|
||||
|
||||
let!(:rows) do
|
||||
let(:rows) do
|
||||
[
|
||||
{ 'acct' => 'foo@bar' },
|
||||
{ 'acct' => 'user@bar' },
|
||||
].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
|
||||
]
|
||||
end
|
||||
|
||||
include_examples 'export failed rows', "foo@bar\nuser@bar\n"
|
||||
|
@ -178,11 +180,11 @@ RSpec.describe Settings::ImportsController do
|
|||
context 'with mutes' do
|
||||
let(:import_type) { 'muting' }
|
||||
|
||||
let!(:rows) do
|
||||
let(:rows) do
|
||||
[
|
||||
{ 'acct' => 'foo@bar' },
|
||||
{ 'acct' => 'user@bar', 'hide_notifications' => false },
|
||||
].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
|
||||
]
|
||||
end
|
||||
|
||||
include_examples 'export failed rows', "Account address,Hide notifications\nfoo@bar,true\nuser@bar,false\n"
|
||||
|
@ -191,11 +193,11 @@ RSpec.describe Settings::ImportsController do
|
|||
context 'with domain blocks' do
|
||||
let(:import_type) { 'domain_blocking' }
|
||||
|
||||
let!(:rows) do
|
||||
let(:rows) do
|
||||
[
|
||||
{ 'domain' => 'bad.domain' },
|
||||
{ 'domain' => 'evil.domain' },
|
||||
].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
|
||||
]
|
||||
end
|
||||
|
||||
include_examples 'export failed rows', "bad.domain\nevil.domain\n"
|
||||
|
@ -204,11 +206,11 @@ RSpec.describe Settings::ImportsController do
|
|||
context 'with bookmarks' do
|
||||
let(:import_type) { 'bookmarks' }
|
||||
|
||||
let!(:rows) do
|
||||
let(:rows) do
|
||||
[
|
||||
{ 'uri' => 'https://foo.com/1' },
|
||||
{ 'uri' => 'https://foo.com/2' },
|
||||
].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
|
||||
]
|
||||
end
|
||||
|
||||
include_examples 'export failed rows', "https://foo.com/1\nhttps://foo.com/2\n"
|
||||
|
@ -217,11 +219,11 @@ RSpec.describe Settings::ImportsController do
|
|||
context 'with lists' do
|
||||
let(:import_type) { 'lists' }
|
||||
|
||||
let!(:rows) do
|
||||
let(:rows) do
|
||||
[
|
||||
{ 'list_name' => 'Amigos', 'acct' => 'user@example.com' },
|
||||
{ 'list_name' => 'Frenemies', 'acct' => 'user@org.org' },
|
||||
].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
|
||||
]
|
||||
end
|
||||
|
||||
include_examples 'export failed rows', "Amigos,user@example.com\nFrenemies,user@org.org\n"
|
||||
|
|
|
@ -1208,15 +1208,21 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
mediaType: 'image/png',
|
||||
url: 'http://example.com/attachment.png',
|
||||
},
|
||||
{
|
||||
type: 'Document',
|
||||
mediaType: 'image/png',
|
||||
url: 'http://example.com/emoji.png',
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
it 'creates status with correctly-ordered media attachments' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
|
||||
expect(status.ordered_media_attachments.map(&:remote_url)).to eq ['http://example.com/attachment.png', 'http://example.com/emoji.png']
|
||||
expect(status.ordered_media_attachment_ids).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -50,6 +50,10 @@ RSpec.describe ActivityPub::Activity::Delete do
|
|||
it 'sends delete activity to followers of rebloggers' do
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'deletes the reblog' do
|
||||
expect { reblog.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ describe Mastodon::CLI::Maintenance do
|
|||
context 'with duplicate accounts' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
choose_local_account_to_keep
|
||||
end
|
||||
|
||||
let(:duplicate_account_username) { 'username' }
|
||||
|
@ -71,21 +72,37 @@ describe Mastodon::CLI::Maintenance do
|
|||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating accounts',
|
||||
'Multiple local accounts were found for',
|
||||
'Restoring index_accounts_on_username_and_domain_lower',
|
||||
'Reindexing textual indexes on accounts…',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_accounts, :count).from(2).to(1)
|
||||
.and change(duplicate_remote_accounts, :count).from(2).to(1)
|
||||
.and change(duplicate_local_accounts, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_accounts
|
||||
def duplicate_remote_accounts
|
||||
Account.where(username: duplicate_account_username, domain: duplicate_account_domain)
|
||||
end
|
||||
|
||||
def duplicate_local_accounts
|
||||
Account.where(username: duplicate_account_username, domain: nil)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :accounts, name: :index_accounts_on_username_and_domain_lower
|
||||
Fabricate(:account, username: duplicate_account_username, domain: duplicate_account_domain)
|
||||
Fabricate.build(:account, username: duplicate_account_username, domain: duplicate_account_domain).save(validate: false)
|
||||
_remote_account = Fabricate(:account, username: duplicate_account_username, domain: duplicate_account_domain)
|
||||
_remote_account_dupe = Fabricate.build(:account, username: duplicate_account_username, domain: duplicate_account_domain).save(validate: false)
|
||||
_local_account = Fabricate(:account, username: duplicate_account_username, domain: nil)
|
||||
_local_account_dupe = Fabricate.build(:account, username: duplicate_account_username, domain: nil).save(validate: false)
|
||||
end
|
||||
|
||||
def choose_local_account_to_keep
|
||||
allow(cli.shell)
|
||||
.to receive(:ask)
|
||||
.with(/Account to keep unchanged/, anything)
|
||||
.and_return('0')
|
||||
.once
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -175,6 +192,407 @@ describe Mastodon::CLI::Maintenance do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with duplicate account_domain_blocks' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:duplicate_domain) { 'example.host' }
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Removing duplicate account domain blocks',
|
||||
'Restoring account domain blocks indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_account_domain_blocks, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_account_domain_blocks
|
||||
AccountDomainBlock.where(account: account, domain: duplicate_domain)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :account_domain_blocks, [:account_id, :domain]
|
||||
Fabricate(:account_domain_block, account: account, domain: duplicate_domain)
|
||||
Fabricate.build(:account_domain_block, account: account, domain: duplicate_domain).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate announcement_reactions' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:announcement) { Fabricate(:announcement) }
|
||||
let(:name) { Fabricate(:custom_emoji).shortcode }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Removing duplicate announcement reactions',
|
||||
'Restoring announcement_reactions indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_announcement_reactions, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_announcement_reactions
|
||||
AnnouncementReaction.where(account: account, announcement: announcement, name: name)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :announcement_reactions, [:account_id, :announcement_id, :name]
|
||||
Fabricate(:announcement_reaction, account: account, announcement: announcement, name: name)
|
||||
Fabricate.build(:announcement_reaction, account: account, announcement: announcement, name: name).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate conversations' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:uri) { 'https://example.host/path' }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating conversations',
|
||||
'Restoring conversations indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_conversations, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_conversations
|
||||
Conversation.where(uri: uri)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :conversations, :uri
|
||||
Fabricate(:conversation, uri: uri)
|
||||
Fabricate.build(:conversation, uri: uri).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate custom_emojis' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:duplicate_shortcode) { 'wowzers' }
|
||||
let(:duplicate_domain) { 'example.host' }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating custom_emojis',
|
||||
'Restoring custom_emojis indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_custom_emojis, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_custom_emojis
|
||||
CustomEmoji.where(shortcode: duplicate_shortcode, domain: duplicate_domain)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :custom_emojis, [:shortcode, :domain]
|
||||
Fabricate(:custom_emoji, shortcode: duplicate_shortcode, domain: duplicate_domain)
|
||||
Fabricate.build(:custom_emoji, shortcode: duplicate_shortcode, domain: duplicate_domain).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate custom_emoji_categories' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:duplicate_name) { 'name_value' }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating custom_emoji_categories',
|
||||
'Restoring custom_emoji_categories indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_custom_emoji_categories, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_custom_emoji_categories
|
||||
CustomEmojiCategory.where(name: duplicate_name)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :custom_emoji_categories, :name
|
||||
Fabricate(:custom_emoji_category, name: duplicate_name)
|
||||
Fabricate.build(:custom_emoji_category, name: duplicate_name).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate domain_allows' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:domain) { 'example.host' }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating domain_allows',
|
||||
'Restoring domain_allows indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_domain_allows, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_domain_allows
|
||||
DomainAllow.where(domain: domain)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :domain_allows, :domain
|
||||
Fabricate(:domain_allow, domain: domain)
|
||||
Fabricate.build(:domain_allow, domain: domain).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate domain_blocks' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:domain) { 'example.host' }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating domain_blocks',
|
||||
'Restoring domain_blocks indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_domain_blocks, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_domain_blocks
|
||||
DomainBlock.where(domain: domain)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :domain_blocks, :domain
|
||||
Fabricate(:domain_block, domain: domain)
|
||||
Fabricate.build(:domain_block, domain: domain).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate email_domain_blocks' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:domain) { 'example.host' }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating email_domain_blocks',
|
||||
'Restoring email_domain_blocks indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_email_domain_blocks, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_email_domain_blocks
|
||||
EmailDomainBlock.where(domain: domain)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :email_domain_blocks, :domain
|
||||
Fabricate(:email_domain_block, domain: domain)
|
||||
Fabricate.build(:email_domain_block, domain: domain).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate media_attachments' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:shortcode) { 'codenam' }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating media_attachments',
|
||||
'Restoring media_attachments indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_media_attachments, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_media_attachments
|
||||
MediaAttachment.where(shortcode: shortcode)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :media_attachments, :shortcode
|
||||
Fabricate(:media_attachment, shortcode: shortcode)
|
||||
Fabricate.build(:media_attachment, shortcode: shortcode).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate preview_cards' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:url) { 'https://example.host/path' }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating preview_cards',
|
||||
'Restoring preview_cards indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_preview_cards, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_preview_cards
|
||||
PreviewCard.where(url: url)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :preview_cards, :url
|
||||
Fabricate(:preview_card, url: url)
|
||||
Fabricate.build(:preview_card, url: url).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate statuses' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:uri) { 'https://example.host/path' }
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating statuses',
|
||||
'Restoring statuses indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_statuses, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_statuses
|
||||
Status.where(uri: uri)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :statuses, :uri
|
||||
Fabricate(:status, account: account, uri: uri)
|
||||
duplicate = Fabricate.build(:status, account: account, uri: uri)
|
||||
duplicate.save(validate: false)
|
||||
Fabricate(:status_pin, account: account, status: duplicate)
|
||||
Fabricate(:status, in_reply_to_id: duplicate.id)
|
||||
Fabricate(:status, reblog_of_id: duplicate.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate tags' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:name) { 'tagname' }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating tags',
|
||||
'Restoring tags indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_tags, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_tags
|
||||
Tag.where(name: name)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :tags, name: 'index_tags_on_name_lower_btree'
|
||||
Fabricate(:tag, name: name)
|
||||
Fabricate.build(:tag, name: name).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate webauthn_credentials' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:external_id) { '123_123_123' }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating webauthn_credentials',
|
||||
'Restoring webauthn_credentials indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_webauthn_credentials, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_webauthn_credentials
|
||||
WebauthnCredential.where(external_id: external_id)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :webauthn_credentials, :external_id
|
||||
Fabricate(:webauthn_credential, external_id: external_id)
|
||||
Fabricate.build(:webauthn_credential, external_id: external_id).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate webhooks' do
|
||||
before do
|
||||
prepare_duplicate_data
|
||||
end
|
||||
|
||||
let(:url) { 'https://example.host/path' }
|
||||
|
||||
it 'runs the deduplication process' do
|
||||
expect { subject }
|
||||
.to output_results(
|
||||
'Deduplicating webhooks',
|
||||
'Restoring webhooks indexes',
|
||||
'Finished!'
|
||||
)
|
||||
.and change(duplicate_webhooks, :count).from(2).to(1)
|
||||
end
|
||||
|
||||
def duplicate_webhooks
|
||||
Webhook.where(url: url)
|
||||
end
|
||||
|
||||
def prepare_duplicate_data
|
||||
ActiveRecord::Base.connection.remove_index :webhooks, :url
|
||||
Fabricate(:webhook, url: url)
|
||||
Fabricate.build(:webhook, url: url).save(validate: false)
|
||||
end
|
||||
end
|
||||
|
||||
def agree_to_backup_warning
|
||||
allow(cli.shell)
|
||||
.to receive(:yes?)
|
||||
|
|
|
@ -13,11 +13,11 @@ RSpec.describe Vacuum::ApplicationsVacuum do
|
|||
let!(:unused_app) { Fabricate(:application, created_at: 1.month.ago) }
|
||||
let!(:recent_app) { Fabricate(:application, created_at: 1.hour.ago) }
|
||||
|
||||
let!(:active_access_token) { Fabricate(:access_token, application: app_with_token) }
|
||||
let!(:active_access_grant) { Fabricate(:access_grant, application: app_with_grant) }
|
||||
let!(:user) { Fabricate(:user, created_by_application: app_with_signup) }
|
||||
|
||||
before do
|
||||
Fabricate(:access_token, application: app_with_token)
|
||||
Fabricate(:access_grant, application: app_with_grant)
|
||||
Fabricate(:user, created_by_application: app_with_signup)
|
||||
|
||||
subject.perform
|
||||
end
|
||||
|
||||
|
|
|
@ -30,5 +30,9 @@ RSpec.describe Vacuum::PreviewCardsVacuum do
|
|||
it 'does not delete attached preview cards' do
|
||||
expect(new_preview_card.reload).to be_persisted
|
||||
end
|
||||
|
||||
it 'does not delete orphaned preview cards in the retention period' do
|
||||
expect(orphaned_preview_card.reload).to be_persisted
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1163,6 +1163,7 @@ RSpec.describe Account do
|
|||
|
||||
it 'returns every usable non-suspended account' do
|
||||
expect(described_class.searchable).to contain_exactly(silenced_local, silenced_remote, local_account, remote_account)
|
||||
expect(described_class.searchable).to_not include(suspended_local, suspended_remote, unconfirmed, unapproved)
|
||||
end
|
||||
|
||||
it 'does not mess with previously-applied scopes' do
|
||||
|
|
|
@ -235,13 +235,17 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
|||
describe '#compute_cutoff_id' do
|
||||
subject { account_statuses_cleanup_policy.compute_cutoff_id }
|
||||
|
||||
let!(:unrelated_status) { Fabricate(:status, created_at: 3.years.ago) }
|
||||
let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) }
|
||||
|
||||
before { Fabricate(:status, created_at: 3.years.ago) }
|
||||
|
||||
context 'when the account has posted multiple toots' do
|
||||
let!(:very_old_status) { Fabricate(:status, created_at: 3.years.ago, account: account) }
|
||||
let!(:old_status) { Fabricate(:status, created_at: 3.weeks.ago, account: account) }
|
||||
let!(:recent_status) { Fabricate(:status, created_at: 2.days.ago, account: account) }
|
||||
let!(:old_status) { Fabricate(:status, created_at: 3.weeks.ago, account: account) }
|
||||
|
||||
before do
|
||||
Fabricate(:status, created_at: 3.years.ago, account: account)
|
||||
Fabricate(:status, created_at: 2.days.ago, account: account)
|
||||
end
|
||||
|
||||
it 'returns the most recent id that is still below policy age' do
|
||||
expect(subject).to eq old_status.id
|
||||
|
@ -270,16 +274,16 @@ RSpec.describe AccountStatusesCleanupPolicy do
|
|||
let!(:faved_secondary) { Fabricate(:status, created_at: 1.year.ago, account: account) }
|
||||
let!(:reblogged_primary) { Fabricate(:status, created_at: 1.year.ago, account: account) }
|
||||
let!(:reblogged_secondary) { Fabricate(:status, created_at: 1.year.ago, account: account) }
|
||||
let!(:recent_status) { Fabricate(:status, created_at: 2.days.ago, account: account) }
|
||||
|
||||
let!(:media_attachment) { Fabricate(:media_attachment, account: account, status: status_with_media) }
|
||||
let!(:status_pin) { Fabricate(:status_pin, account: account, status: pinned_status) }
|
||||
let!(:favourite) { Fabricate(:favourite, account: account, status: self_faved) }
|
||||
let!(:bookmark) { Fabricate(:bookmark, account: account, status: self_bookmarked) }
|
||||
let!(:recent_status) { Fabricate(:status, created_at: 2.days.ago, account: account) }
|
||||
|
||||
let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) }
|
||||
|
||||
before do
|
||||
Fabricate(:media_attachment, account: account, status: status_with_media)
|
||||
Fabricate(:status_pin, account: account, status: pinned_status)
|
||||
Fabricate(:favourite, account: account, status: self_faved)
|
||||
Fabricate(:bookmark, account: account, status: self_bookmarked)
|
||||
|
||||
faved_primary.status_stat.update(favourites_count: 4)
|
||||
faved_secondary.status_stat.update(favourites_count: 5)
|
||||
reblogged_primary.status_stat.update(reblogs_count: 4)
|
||||
|
|
|
@ -28,7 +28,7 @@ RSpec.describe CanonicalEmailBlock do
|
|||
end
|
||||
|
||||
describe '.block?' do
|
||||
let!(:canonical_email_block) { Fabricate(:canonical_email_block, email: 'foo@bar.com') }
|
||||
before { Fabricate(:canonical_email_block, email: 'foo@bar.com') }
|
||||
|
||||
it 'returns true for the same email' do
|
||||
expect(described_class.block?('foo@bar.com')).to be true
|
||||
|
|
|
@ -516,17 +516,29 @@ RSpec.describe Status do
|
|||
|
||||
context 'when given one tag' do
|
||||
it 'returns the expected statuses' do
|
||||
expect(described_class.tagged_with([tag_cats.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_tag_cats.id, status_with_all_tags.id)
|
||||
expect(described_class.tagged_with([tag_dogs.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_tag_dogs.id, status_with_all_tags.id)
|
||||
expect(described_class.tagged_with([tag_zebras.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_tagged_with_zebras.id, status_with_all_tags.id)
|
||||
expect(described_class.tagged_with([tag_cats.id]))
|
||||
.to include(status_with_tag_cats, status_with_all_tags)
|
||||
.and not_include(status_without_tags)
|
||||
expect(described_class.tagged_with([tag_dogs.id]))
|
||||
.to include(status_with_tag_dogs, status_with_all_tags)
|
||||
.and not_include(status_without_tags)
|
||||
expect(described_class.tagged_with([tag_zebras.id]))
|
||||
.to include(status_tagged_with_zebras, status_with_all_tags)
|
||||
.and not_include(status_without_tags)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when given multiple tags' do
|
||||
it 'returns the expected statuses' do
|
||||
expect(described_class.tagged_with([tag_cats.id, tag_dogs.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_tag_cats.id, status_with_tag_dogs.id, status_with_all_tags.id)
|
||||
expect(described_class.tagged_with([tag_cats.id, tag_zebras.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_tag_cats.id, status_tagged_with_zebras.id, status_with_all_tags.id)
|
||||
expect(described_class.tagged_with([tag_dogs.id, tag_zebras.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_tag_dogs.id, status_tagged_with_zebras.id, status_with_all_tags.id)
|
||||
expect(described_class.tagged_with([tag_cats.id, tag_dogs.id]))
|
||||
.to include(status_with_tag_cats, status_with_tag_dogs, status_with_all_tags)
|
||||
.and not_include(status_without_tags)
|
||||
expect(described_class.tagged_with([tag_cats.id, tag_zebras.id]))
|
||||
.to include(status_with_tag_cats, status_tagged_with_zebras, status_with_all_tags)
|
||||
.and not_include(status_without_tags)
|
||||
expect(described_class.tagged_with([tag_dogs.id, tag_zebras.id]))
|
||||
.to include(status_with_tag_dogs, status_tagged_with_zebras, status_with_all_tags)
|
||||
.and not_include(status_without_tags)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -543,17 +555,26 @@ RSpec.describe Status do
|
|||
|
||||
context 'when given one tag' do
|
||||
it 'returns the expected statuses' do
|
||||
expect(described_class.tagged_with_all([tag_cats.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_tag_cats.id, status_with_all_tags.id)
|
||||
expect(described_class.tagged_with_all([tag_dogs.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_tag_dogs.id, status_with_all_tags.id)
|
||||
expect(described_class.tagged_with_all([tag_zebras.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_tagged_with_zebras.id)
|
||||
expect(described_class.tagged_with_all([tag_cats.id]))
|
||||
.to include(status_with_tag_cats, status_with_all_tags)
|
||||
.and not_include(status_without_tags)
|
||||
expect(described_class.tagged_with_all([tag_dogs.id]))
|
||||
.to include(status_with_tag_dogs, status_with_all_tags)
|
||||
.and not_include(status_without_tags)
|
||||
expect(described_class.tagged_with_all([tag_zebras.id]))
|
||||
.to include(status_tagged_with_zebras)
|
||||
.and not_include(status_without_tags)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when given multiple tags' do
|
||||
it 'returns the expected statuses' do
|
||||
expect(described_class.tagged_with_all([tag_cats.id, tag_dogs.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_all_tags.id)
|
||||
expect(described_class.tagged_with_all([tag_cats.id, tag_zebras.id]).reorder(:id).pluck(:id).uniq).to eq []
|
||||
expect(described_class.tagged_with_all([tag_dogs.id, tag_zebras.id]).reorder(:id).pluck(:id).uniq).to eq []
|
||||
expect(described_class.tagged_with_all([tag_cats.id, tag_dogs.id]))
|
||||
.to include(status_with_all_tags)
|
||||
expect(described_class.tagged_with_all([tag_cats.id, tag_zebras.id]))
|
||||
.to eq []
|
||||
expect(described_class.tagged_with_all([tag_dogs.id, tag_zebras.id]))
|
||||
.to eq []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -570,17 +591,29 @@ RSpec.describe Status do
|
|||
|
||||
context 'when given one tag' do
|
||||
it 'returns the expected statuses' do
|
||||
expect(described_class.tagged_with_none([tag_cats.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_tag_dogs.id, status_tagged_with_zebras.id, status_without_tags.id)
|
||||
expect(described_class.tagged_with_none([tag_dogs.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_tag_cats.id, status_tagged_with_zebras.id, status_without_tags.id)
|
||||
expect(described_class.tagged_with_none([tag_zebras.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_tag_cats.id, status_with_tag_dogs.id, status_without_tags.id)
|
||||
expect(described_class.tagged_with_none([tag_cats.id]))
|
||||
.to include(status_with_tag_dogs, status_tagged_with_zebras, status_without_tags)
|
||||
.and not_include(status_with_all_tags)
|
||||
expect(described_class.tagged_with_none([tag_dogs.id]))
|
||||
.to include(status_with_tag_cats, status_tagged_with_zebras, status_without_tags)
|
||||
.and not_include(status_with_all_tags)
|
||||
expect(described_class.tagged_with_none([tag_zebras.id]))
|
||||
.to include(status_with_tag_cats, status_with_tag_dogs, status_without_tags)
|
||||
.and not_include(status_with_all_tags)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when given multiple tags' do
|
||||
it 'returns the expected statuses' do
|
||||
expect(described_class.tagged_with_none([tag_cats.id, tag_dogs.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_tagged_with_zebras.id, status_without_tags.id)
|
||||
expect(described_class.tagged_with_none([tag_cats.id, tag_zebras.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_tag_dogs.id, status_without_tags.id)
|
||||
expect(described_class.tagged_with_none([tag_dogs.id, tag_zebras.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status_with_tag_cats.id, status_without_tags.id)
|
||||
expect(described_class.tagged_with_none([tag_cats.id, tag_dogs.id]))
|
||||
.to include(status_tagged_with_zebras, status_without_tags)
|
||||
.and not_include(status_with_all_tags)
|
||||
expect(described_class.tagged_with_none([tag_cats.id, tag_zebras.id]))
|
||||
.to include(status_with_tag_dogs, status_without_tags)
|
||||
.and not_include(status_with_all_tags)
|
||||
expect(described_class.tagged_with_none([tag_dogs.id, tag_zebras.id]))
|
||||
.to include(status_with_tag_cats, status_without_tags)
|
||||
.and not_include(status_with_all_tags)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -422,6 +422,7 @@ RSpec.describe User do
|
|||
|
||||
it 'deactivates all sessions' do
|
||||
expect(user.session_activations.count).to eq 0
|
||||
expect { session_activation.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'revokes all access tokens' do
|
||||
|
@ -430,6 +431,7 @@ RSpec.describe User do
|
|||
|
||||
it 'removes push subscriptions' do
|
||||
expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0
|
||||
expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -153,6 +153,7 @@ RSpec::Sidekiq.configure do |config|
|
|||
end
|
||||
|
||||
RSpec::Matchers.define_negated_matcher :not_change, :change
|
||||
RSpec::Matchers.define_negated_matcher :not_include, :include
|
||||
|
||||
def request_fixture(name)
|
||||
Rails.root.join('spec', 'fixtures', 'requests', name).read
|
||||
|
|
332
spec/requests/signature_verification_spec.rb
Normal file
332
spec/requests/signature_verification_spec.rb
Normal file
|
@ -0,0 +1,332 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'signature verification concern' do
|
||||
before do
|
||||
stub_tests_controller
|
||||
|
||||
# Signature checking is time-dependent, so travel to a fixed date
|
||||
travel_to '2023-12-20T10:00:00Z'
|
||||
end
|
||||
|
||||
after { Rails.application.reload_routes! }
|
||||
|
||||
# Include the private key so the tests can be easily adjusted and reviewed
|
||||
let(:actor_keypair) do
|
||||
OpenSSL::PKey.read(<<~PEM_TEXT)
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAqIAYvNFGbZ5g4iiK6feSdXD4bDStFM58A7tHycYXaYtzZQpI
|
||||
eHXAmaXuZzXIwtrP4N0gIk8JNwZvXj2UPS+S07t0V9wNK94he01LV5EMz/GN4eNn
|
||||
FmDL64HIEuKLvV8TvgjbUPRD6Y5X0UpKi2ZIFLSb96Q5w0Z/k7ntpVKV52y8kz5F
|
||||
jr/O/0JuHryZe0yItzJh8kzFfeMf0EXzfSnaKvT7P9jhgC6uTre+jXyvVZjiHDrn
|
||||
qvvucdI3I7DRfXo1OqARBrLjy+TdseUAjNYJ+OuPRI1URIWQI01DCHqcohVu9+Ar
|
||||
+BiCjFp3ua+XMuJvrvbD61d1Fvig/9nbBRR+8QIDAQABAoIBAAgySHnFWI6gItR3
|
||||
fkfiqIm80cHCN3Xk1C6iiVu+3oBOZbHpW9R7vl9e/WOA/9O+LPjiSsQOegtWnVvd
|
||||
RRjrl7Hj20VDlZKv5Mssm6zOGAxksrcVbqwdj+fUJaNJCL0AyyseH0x/IE9T8rDC
|
||||
I1GH+3tB3JkhkIN/qjipdX5ab8MswEPu8IC4ViTpdBgWYY/xBcAHPw4xuL0tcwzh
|
||||
FBlf4DqoEVQo8GdK5GAJ2Ny0S4xbXHUURzx/R4y4CCts7niAiLGqd9jmLU1kUTMk
|
||||
QcXfQYK6l+unLc7wDYAz7sFEHh04M48VjWwiIZJnlCqmQbLda7uhhu8zkF1DqZTu
|
||||
ulWDGQECgYEA0TIAc8BQBVab979DHEEmMdgqBwxLY3OIAk0b+r50h7VBGWCDPRsC
|
||||
STD73fQY3lNet/7/jgSGwwAlAJ5PpMXxXiZAE3bUwPmHzgF7pvIOOLhA8O07tHSO
|
||||
L2mvQe6NPzjZ+6iAO2U9PkClxcvGvPx2OBvisfHqZLmxC9PIVxzruQECgYEAzjM6
|
||||
BTUXa6T/qHvLFbN699BXsUOGmHBGaLRapFDBfVvgZrwqYQcZpBBhesLdGTGSqwE7
|
||||
gWsITPIJ+Ldo+38oGYyVys+w/V67q6ud7hgSDTW3hSvm+GboCjk6gzxlt9hQ0t9X
|
||||
8vfDOYhEXvVUJNv3mYO60ENqQhILO4bQ0zi+VfECgYBb/nUccfG+pzunU0Cb6Dp3
|
||||
qOuydcGhVmj1OhuXxLFSDG84Tazo7juvHA9mp7VX76mzmDuhpHPuxN2AzB2SBEoE
|
||||
cSW0aYld413JRfWukLuYTc6hJHIhBTCRwRQFFnae2s1hUdQySm8INT2xIc+fxBXo
|
||||
zrp+Ljg5Wz90SAnN5TX0AQKBgDaatDOq0o/r+tPYLHiLtfWoE4Dau+rkWJDjqdk3
|
||||
lXWn/e3WyHY3Vh/vQpEqxzgju45TXjmwaVtPATr+/usSykCxzP0PMPR3wMT+Rm1F
|
||||
rIoY/odij+CaB7qlWwxj0x/zRbwB7x1lZSp4HnrzBpxYL+JUUwVRxPLIKndSBTza
|
||||
GvVRAoGBAIVBcNcRQYF4fvZjDKAb4fdBsEuHmycqtRCsnkGOz6ebbEQznSaZ0tZE
|
||||
+JuouZaGjyp8uPjNGD5D7mIGbyoZ3KyG4mTXNxDAGBso1hrNDKGBOrGaPhZx8LgO
|
||||
4VXJ+ybXrATf4jr8ccZYsZdFpOphPzz+j55Mqg5vac5P1XjmsGTb
|
||||
-----END RSA PRIVATE KEY-----
|
||||
PEM_TEXT
|
||||
end
|
||||
|
||||
context 'without a Signature header' do
|
||||
it 'does not treat the request as signed' do
|
||||
get '/activitypub/success'
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json).to match(
|
||||
signed_request: false,
|
||||
signature_actor_id: nil,
|
||||
error: 'Request not signed'
|
||||
)
|
||||
end
|
||||
|
||||
context 'when a signature is required' do
|
||||
it 'returns http unauthorized with appropriate error' do
|
||||
get '/activitypub/signature_required'
|
||||
|
||||
expect(response).to have_http_status(401)
|
||||
expect(body_as_json).to match(
|
||||
error: 'Request not signed'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an HTTP Signature from a known account' do
|
||||
let!(:actor) { Fabricate(:account, domain: 'remote.domain', uri: 'https://remote.domain/users/bob', private_key: nil, public_key: actor_keypair.public_key.to_pem) }
|
||||
|
||||
context 'with a valid signature on a GET request' do
|
||||
let(:signature_header) do
|
||||
'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="' # rubocop:disable Layout/LineLength
|
||||
end
|
||||
|
||||
it 'successfuly verifies signature', :aggregate_failures do
|
||||
expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' })
|
||||
|
||||
get '/activitypub/success', headers: {
|
||||
'Host' => 'www.example.com',
|
||||
'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
|
||||
'Signature' => signature_header,
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json).to match(
|
||||
signed_request: true,
|
||||
signature_actor_id: actor.id.to_s
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a mismatching path' do
|
||||
it 'fails to verify signature', :aggregate_failures do
|
||||
get '/activitypub/alternative-path', headers: {
|
||||
'Host' => 'www.example.com',
|
||||
'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
|
||||
'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength
|
||||
}
|
||||
|
||||
expect(body_as_json).to match(
|
||||
signed_request: true,
|
||||
signature_actor_id: nil,
|
||||
error: anything
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a mismatching method' do
|
||||
it 'fails to verify signature', :aggregate_failures do
|
||||
post '/activitypub/success', headers: {
|
||||
'Host' => 'www.example.com',
|
||||
'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
|
||||
'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength
|
||||
}
|
||||
|
||||
expect(body_as_json).to match(
|
||||
signed_request: true,
|
||||
signature_actor_id: nil,
|
||||
error: anything
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unparsable date' do
|
||||
let(:signature_header) do
|
||||
'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="d4B7nfx8RJcfdJDu1J//5WzPzK/hgtPkdzZx49lu5QhnE7qdV3lgyVimmhCFrO16bwvzIp9iRMyRLkNFxLiEeVaa1gqeKbldGSnU0B0OMjx7rFBa65vLuzWQOATDitVGiBEYqoK4v0DMuFCz2DtFaA/DIUZ3sty8bZ/Ea3U1nByLOO6MacARA3zhMSI0GNxGqsSmZmG0hPLavB3jIXoE3IDoQabMnC39jrlcO/a8h1iaxBm2WD8TejrImJullgqlJIFpKhIHI3ipQkvTGPlm9dx0y+beM06qBvWaWQcmT09eRIUefVsOAzIhUtS/7FVb/URhZvircIJDa7vtiFcmZQ=="' # rubocop:disable Layout/LineLength
|
||||
end
|
||||
|
||||
it 'fails to verify signature', :aggregate_failures do
|
||||
expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'wrong date', 'Host' => 'www.example.com' })
|
||||
|
||||
get '/activitypub/success', headers: {
|
||||
'Host' => 'www.example.com',
|
||||
'Date' => 'wrong date',
|
||||
'Signature' => signature_header,
|
||||
}
|
||||
|
||||
expect(body_as_json).to match(
|
||||
signed_request: true,
|
||||
signature_actor_id: nil,
|
||||
error: 'Invalid Date header: not RFC 2616 compliant date: "wrong date"'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a request older than a day' do
|
||||
let(:signature_header) do
|
||||
'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="G1NuJv4zgoZ3B/ZIjzDWZHK4RC+5pYee74q8/LJEMCWXhcnAomcb9YHaqk1QYfQvcBUIXw3UZ3Q9xO8F9y0i8G5mzJHfQ+OgHqCoJk8EmGwsUXJMh5s1S5YFCRt8TT12TmJZz0VMqLq85ubueSYBM7QtUE/FzFIVLvz4RysgXxaXQKzdnM6+gbUEEKdCURpXdQt2NXQhp4MAmZH3+0lQoR6VxdsK0hx0Ji2PNp1nuqFTlYqNWZazVdLBN+9rETLRmvGXknvg9jOxTTppBVWnkAIl26HtLS3wwFVvz4pJzi9OQDOvLziehVyLNbU61hky+oJ215e2HuKSe2hxHNl1MA=="' # rubocop:disable Layout/LineLength
|
||||
end
|
||||
|
||||
it 'fails to verify signature', :aggregate_failures do
|
||||
expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'Wed, 18 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' })
|
||||
|
||||
get '/activitypub/success', headers: {
|
||||
'Host' => 'www.example.com',
|
||||
'Date' => 'Wed, 18 Dec 2023 10:00:00 GMT',
|
||||
'Signature' => signature_header,
|
||||
}
|
||||
|
||||
expect(body_as_json).to match(
|
||||
signed_request: true,
|
||||
signature_actor_id: nil,
|
||||
error: 'Signed request date outside acceptable time window'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a valid signature on a POST request' do
|
||||
let(:digest_header) { 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' }
|
||||
let(:signature_header) do
|
||||
'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="' # rubocop:disable Layout/LineLength
|
||||
end
|
||||
|
||||
it 'successfuly verifies signature', :aggregate_failures do
|
||||
expect(digest_header).to eq digest_value('Hello world')
|
||||
expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'post /activitypub/success', { 'Host' => 'www.example.com', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Digest' => digest_header })
|
||||
|
||||
post '/activitypub/success', params: 'Hello world', headers: {
|
||||
'Host' => 'www.example.com',
|
||||
'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
|
||||
'Digest' => digest_header,
|
||||
'Signature' => signature_header,
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json).to match(
|
||||
signed_request: true,
|
||||
signature_actor_id: actor.id.to_s
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the Digest of a POST request is not signed' do
|
||||
let(:digest_header) { 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' }
|
||||
let(:signature_header) do
|
||||
'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date (request-target)",signature="CPD704CG8aCm8X8qIP8kkkiGp1qwFLk/wMVQHOGP0Txxan8c2DZtg/KK7eN8RG8tHx8br/yS2hJs51x4kXImYukGzNJd7ihE3T8lp+9RI1tCcdobTzr/VcVJHDFySdQkg266GCMijRQRZfNvqlJLiisr817PI+gNVBI5qV+vnVd1XhWCEZ+YSmMe8UqYARXAYNqMykTheojqGpTeTFGPUpTQA2Fmt2BipwIjcFDm2Hpihl2kB0MUS0x3zPmHDuadvzoBbN6m3usPDLgYrpALlh+wDs1dYMntcwdwawRKY1oE1XNtgOSum12wntDq3uYL4gya2iPdcw3c929b4koUzw=="' # rubocop:disable Layout/LineLength
|
||||
end
|
||||
|
||||
it 'fails to verify signature', :aggregate_failures do
|
||||
expect(digest_header).to eq digest_value('Hello world')
|
||||
expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'post /activitypub/success', { 'Host' => 'www.example.com', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT' })
|
||||
|
||||
post '/activitypub/success', params: 'Hello world', headers: {
|
||||
'Host' => 'www.example.com',
|
||||
'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
|
||||
'Digest' => digest_header,
|
||||
'Signature' => signature_header,
|
||||
}
|
||||
|
||||
expect(body_as_json).to match(
|
||||
signed_request: true,
|
||||
signature_actor_id: nil,
|
||||
error: 'Mastodon requires the Digest header to be signed when doing a POST request'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a tampered body on a POST request' do
|
||||
let(:digest_header) { 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' }
|
||||
let(:signature_header) do
|
||||
'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="' # rubocop:disable Layout/LineLength
|
||||
end
|
||||
|
||||
it 'fails to verify signature', :aggregate_failures do
|
||||
expect(digest_header).to_not eq digest_value('Hello world!')
|
||||
expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'post /activitypub/success', { 'Host' => 'www.example.com', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Digest' => digest_header })
|
||||
|
||||
post '/activitypub/success', params: 'Hello world!', headers: {
|
||||
'Host' => 'www.example.com',
|
||||
'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
|
||||
'Digest' => 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=',
|
||||
'Signature' => signature_header,
|
||||
}
|
||||
|
||||
expect(body_as_json).to match(
|
||||
signed_request: true,
|
||||
signature_actor_id: nil,
|
||||
error: 'Invalid Digest value. Computed SHA-256 digest: wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro=; given: ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw='
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a tampered path in a POST request' do
|
||||
it 'fails to verify signature', :aggregate_failures do
|
||||
post '/activitypub/alternative-path', params: 'Hello world', headers: {
|
||||
'Host' => 'www.example.com',
|
||||
'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
|
||||
'Digest' => 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=',
|
||||
'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="', # rubocop:disable Layout/LineLength
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json).to match(
|
||||
signed_request: true,
|
||||
signature_actor_id: nil,
|
||||
error: anything
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an inaccessible key' do
|
||||
before do
|
||||
stub_request(:get, 'https://remote.domain/users/alice#main-key').to_return(status: 404)
|
||||
end
|
||||
|
||||
it 'fails to verify signature', :aggregate_failures do
|
||||
get '/activitypub/success', headers: {
|
||||
'Host' => 'www.example.com',
|
||||
'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT',
|
||||
'Signature' => 'keyId="https://remote.domain/users/alice#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength
|
||||
}
|
||||
|
||||
expect(body_as_json).to match(
|
||||
signed_request: true,
|
||||
signature_actor_id: nil,
|
||||
error: 'Unable to fetch key JSON at https://remote.domain/users/alice#main-key'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def stub_tests_controller
|
||||
stub_const('ActivityPub::TestsController', activitypub_tests_controller)
|
||||
|
||||
Rails.application.routes.draw do
|
||||
# NOTE: RouteSet#draw removes all routes, so we need to re-insert one
|
||||
resource :instance_actor, path: 'actor', only: [:show]
|
||||
|
||||
match :via => [:get, :post], '/activitypub/success' => 'activitypub/tests#success'
|
||||
match :via => [:get, :post], '/activitypub/alternative-path' => 'activitypub/tests#alternative_success'
|
||||
match :via => [:get, :post], '/activitypub/signature_required' => 'activitypub/tests#signature_required'
|
||||
end
|
||||
end
|
||||
|
||||
def activitypub_tests_controller
|
||||
Class.new(ApplicationController) do
|
||||
include SignatureVerification
|
||||
|
||||
before_action :require_actor_signature!, only: [:signature_required]
|
||||
|
||||
def success
|
||||
render json: {
|
||||
signed_request: signed_request?,
|
||||
signature_actor_id: signed_request_actor&.id&.to_s,
|
||||
}.merge(signature_verification_failure_reason || {})
|
||||
end
|
||||
|
||||
alias_method :alternative_success, :success
|
||||
alias_method :signature_required, :success
|
||||
end
|
||||
end
|
||||
|
||||
def digest_value(body)
|
||||
"SHA-256=#{Digest::SHA256.base64digest(body)}"
|
||||
end
|
||||
|
||||
def build_signature_string(keypair, key_id, request_target, headers)
|
||||
algorithm = 'rsa-sha256'
|
||||
signed_headers = headers.merge({ '(request-target)' => request_target })
|
||||
signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
|
||||
signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
|
||||
|
||||
"keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
|
||||
end
|
||||
end
|
|
@ -39,6 +39,13 @@ describe AccountStatusesCleanupService, type: :service do
|
|||
it 'actually deletes the statuses' do
|
||||
subject.call(account_policy, 10)
|
||||
expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])).to be_nil
|
||||
expect { recent_status.reload }.to_not raise_error
|
||||
end
|
||||
|
||||
it 'preserves recent and unrelated statuses' do
|
||||
subject.call(account_policy, 10)
|
||||
expect { unrelated_status.reload }.to_not raise_error
|
||||
expect { recent_status.reload }.to_not raise_error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
|||
'https://example.com/account/pinned/unknown-inlined',
|
||||
'https://example.com/account/pinned/unknown-reachable'
|
||||
)
|
||||
expect(actor.pinned_statuses).to_not include(known_status)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do
|
|||
subject { described_class.new }
|
||||
|
||||
let!(:sender) { Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar') }
|
||||
let!(:recipient) { Fabricate(:account) }
|
||||
|
||||
let(:existing_status) { nil }
|
||||
|
||||
|
|
|
@ -283,7 +283,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
|
|||
end
|
||||
|
||||
context 'when account is not suspended' do
|
||||
subject { described_class.new.call('alice', 'example.com', payload) }
|
||||
subject { described_class.new.call(account.username, account.domain, payload) }
|
||||
|
||||
let!(:account) { Fabricate(:account, username: 'alice', domain: 'example.com') }
|
||||
|
||||
|
|
|
@ -242,7 +242,8 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
|
|||
it 'does not process forged payload' do
|
||||
allow(ActivityPub::Activity).to receive(:factory)
|
||||
|
||||
subject.call(json, forwarder)
|
||||
expect { subject.call(json, forwarder) }
|
||||
.to_not change(actor.reload.statuses, :count)
|
||||
|
||||
expect(ActivityPub::Activity).to_not have_received(:factory).with(
|
||||
hash_including(
|
||||
|
|
|
@ -10,7 +10,7 @@ RSpec.describe BatchedRemoveStatusService, type: :service do
|
|||
let!(:jeff) { Fabricate(:account) }
|
||||
let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
|
||||
let(:status_alice_hello) { PostStatusService.new.call(alice, text: 'Hello @bob@example.com') }
|
||||
let(:status_alice_hello) { PostStatusService.new.call(alice, text: "Hello @#{bob.pretty_acct}") }
|
||||
let(:status_alice_other) { PostStatusService.new.call(alice, text: 'Another status') }
|
||||
|
||||
before do
|
||||
|
|
|
@ -10,7 +10,10 @@ RSpec.describe BlockDomainService, type: :service do
|
|||
let!(:bad_status_with_attachment) { Fabricate(:status, account: bad_account, text: 'Hahaha') }
|
||||
let!(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status_with_attachment, file: attachment_fixture('attachment.jpg')) }
|
||||
let!(:already_banned_account) { Fabricate(:account, username: 'badguy', domain: 'evil.org', suspended: true, silenced: true) }
|
||||
let!(:bad_friend) { Fabricate(:friend_domain, domain: 'evil.org', inbox_url: 'https://evil.org/inbox', active_state: :accepted, passive_state: :accepted) }
|
||||
|
||||
before do
|
||||
Fabricate(:friend_domain, domain: 'evil.org', inbox_url: 'https://evil.org/inbox', active_state: :accepted, passive_state: :accepted)
|
||||
end
|
||||
|
||||
describe 'for a suspension' do
|
||||
before do
|
||||
|
@ -25,19 +28,19 @@ RSpec.describe BlockDomainService, type: :service do
|
|||
end
|
||||
|
||||
it 'removes remote accounts from that domain' do
|
||||
expect(Account.find_remote('badguy666', 'evil.org').suspended?).to be true
|
||||
expect(bad_account.reload.suspended?).to be true
|
||||
end
|
||||
|
||||
it 'records suspension date appropriately' do
|
||||
expect(Account.find_remote('badguy666', 'evil.org').suspended_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at
|
||||
expect(bad_account.reload.suspended_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at
|
||||
end
|
||||
|
||||
it 'keeps already-banned accounts banned' do
|
||||
expect(Account.find_remote('badguy', 'evil.org').suspended?).to be true
|
||||
expect(already_banned_account.reload.suspended?).to be true
|
||||
end
|
||||
|
||||
it 'does not overwrite suspension date of already-banned accounts' do
|
||||
expect(Account.find_remote('badguy', 'evil.org').suspended_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at
|
||||
expect(already_banned_account.reload.suspended_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at
|
||||
end
|
||||
|
||||
it 'removes the remote accounts\'s statuses and media attachments' do
|
||||
|
@ -72,19 +75,19 @@ RSpec.describe BlockDomainService, type: :service do
|
|||
end
|
||||
|
||||
it 'silences remote accounts from that domain' do
|
||||
expect(Account.find_remote('badguy666', 'evil.org').silenced?).to be true
|
||||
expect(bad_account.reload.silenced?).to be true
|
||||
end
|
||||
|
||||
it 'records suspension date appropriately' do
|
||||
expect(Account.find_remote('badguy666', 'evil.org').silenced_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at
|
||||
expect(bad_account.reload.silenced_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at
|
||||
end
|
||||
|
||||
it 'keeps already-banned accounts banned' do
|
||||
expect(Account.find_remote('badguy', 'evil.org').silenced?).to be true
|
||||
expect(already_banned_account.reload.silenced?).to be true
|
||||
end
|
||||
|
||||
it 'does not overwrite suspension date of already-banned accounts' do
|
||||
expect(Account.find_remote('badguy', 'evil.org').silenced_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at
|
||||
expect(already_banned_account.reload.silenced_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at
|
||||
end
|
||||
|
||||
it 'leaves the domains status and attachments, but clears media' do
|
||||
|
|
|
@ -271,14 +271,15 @@ RSpec.describe BulkImportService do
|
|||
let(:import_type) { 'domain_blocking' }
|
||||
let(:overwrite) { false }
|
||||
|
||||
let!(:rows) do
|
||||
let(:rows) do
|
||||
[
|
||||
{ 'domain' => 'blocked.com' },
|
||||
{ 'domain' => 'to_block.com' },
|
||||
].map { |data| import.rows.create!(data: data) }
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
rows.each { |data| import.rows.create!(data: data) }
|
||||
account.block_domain!('alreadyblocked.com')
|
||||
account.block_domain!('blocked.com')
|
||||
end
|
||||
|
@ -298,14 +299,15 @@ RSpec.describe BulkImportService do
|
|||
let(:import_type) { 'domain_blocking' }
|
||||
let(:overwrite) { true }
|
||||
|
||||
let!(:rows) do
|
||||
let(:rows) do
|
||||
[
|
||||
{ 'domain' => 'blocked.com' },
|
||||
{ 'domain' => 'to_block.com' },
|
||||
].map { |data| import.rows.create!(data: data) }
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
rows.each { |data| import.rows.create!(data: data) }
|
||||
account.block_domain!('alreadyblocked.com')
|
||||
account.block_domain!('blocked.com')
|
||||
end
|
||||
|
|
|
@ -48,30 +48,14 @@ RSpec.describe DeleteAccountService, type: :service do
|
|||
let!(:account_note) { Fabricate(:account_note, account: account) }
|
||||
|
||||
it 'deletes associated owned and target records and target notifications' do
|
||||
expect { subject }
|
||||
.to delete_associated_owned_records
|
||||
.and delete_associated_target_records
|
||||
.and delete_associated_target_notifications
|
||||
subject
|
||||
|
||||
expect_deletion_of_associated_owned_records
|
||||
expect_deletion_of_associated_target_records
|
||||
expect_deletion_of_associated_target_notifications
|
||||
end
|
||||
|
||||
def delete_associated_owned_records
|
||||
change do
|
||||
[
|
||||
account.statuses,
|
||||
account.media_attachments,
|
||||
account.notifications,
|
||||
account.favourites,
|
||||
account.emoji_reactions,
|
||||
account.bookmarks,
|
||||
account.active_relationships,
|
||||
account.passive_relationships,
|
||||
account.polls,
|
||||
account.account_notes,
|
||||
].map(&:count)
|
||||
end.from([3, 1, 1, 1, 1, 1, 2, 2, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
|
||||
end
|
||||
|
||||
it 'deletes associated owned record groups' do
|
||||
it 'deletes associated owned record groups' do # rubocop:disable RSpec/MultipleExpectations
|
||||
expect { subject }.to change {
|
||||
[
|
||||
account.owned_lists,
|
||||
|
@ -80,62 +64,81 @@ RSpec.describe DeleteAccountService, type: :service do
|
|||
account.bookmark_categories,
|
||||
].map(&:count)
|
||||
}.from([1, 1, 1, 1]).to([0, 0, 0, 0])
|
||||
expect { list_target_account.reload }.to_not raise_error
|
||||
expect { bookmark_category_status.status.reload }.to_not raise_error
|
||||
expect { antenna_account.account.reload }.to_not raise_error
|
||||
expect { circle_account.account.reload }.to_not raise_error
|
||||
expect { list.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { list_account.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { antenna_account.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { circle_account.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { circle_status.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { bookmark_category_status.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
def delete_associated_target_records
|
||||
change(account_pins_for_account, :count).from(1).to(0)
|
||||
def expect_deletion_of_associated_owned_records
|
||||
expect { status.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { status_with_mention.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { mention.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { media_attachment.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { favourite.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { emoji_reaction.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { bookmark.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { active_relationship.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { passive_relationship.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { poll.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { poll_vote.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { account_note.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
def expect_deletion_of_associated_target_records
|
||||
expect { endorsement.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
def account_pins_for_account
|
||||
AccountPin.where(target_account: account)
|
||||
end
|
||||
|
||||
def delete_associated_target_notifications
|
||||
change do
|
||||
%w(
|
||||
poll favourite emoji_reaction status mention follow
|
||||
).map { |type| Notification.where(type: type).count }
|
||||
end.from([1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0])
|
||||
def expect_deletion_of_associated_target_notifications
|
||||
expect { favourite_notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { follow_notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { mention_notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { poll_notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { status_notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { emoji_reaction_notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
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)
|
||||
stub_request(:post, remote_alice.inbox_url).to_return(status: 201)
|
||||
stub_request(:post, remote_bob.inbox_url).to_return(status: 201)
|
||||
end
|
||||
|
||||
let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', domain: 'alice.com', protocol: :activitypub) }
|
||||
let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', domain: 'bob.com', protocol: :activitypub) }
|
||||
|
||||
include_examples 'common behavior' do
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:local_follower) { Fabricate(:account) }
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:local_follower) { Fabricate(:account) }
|
||||
|
||||
it 'sends a delete actor activity to all known inboxes' do
|
||||
subject
|
||||
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
|
||||
expect(a_request(:post, remote_alice.inbox_url)).to have_been_made.once
|
||||
expect(a_request(:post, remote_bob.inbox_url)).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)
|
||||
stub_request(:post, account.inbox_url).to_return(status: 201)
|
||||
end
|
||||
|
||||
include_examples 'common behavior' do
|
||||
let!(:account) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub, domain: 'bob.com') }
|
||||
let!(:local_follower) { Fabricate(:account) }
|
||||
let(:account) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub, domain: 'bob.com') }
|
||||
let(:local_follower) { Fabricate(:account) }
|
||||
|
||||
it 'sends expected activities to followed and follower inboxes' do
|
||||
subject
|
||||
|
|
|
@ -190,7 +190,7 @@ RSpec.describe ImportService, type: :service do
|
|||
|
||||
# Make sure to not actually go to the remote server
|
||||
before do
|
||||
stub_request(:post, 'https://թութ.հայ/inbox').to_return(status: 200)
|
||||
stub_request(:post, nare.inbox_url).to_return(status: 200)
|
||||
end
|
||||
|
||||
it 'follows the listed account' do
|
||||
|
|
|
@ -67,9 +67,10 @@ RSpec.describe NotifyService, type: :service do
|
|||
|
||||
context 'when the message chain is initiated by recipient, but is not direct message' do
|
||||
let(:reply_to) { Fabricate(:status, account: recipient) }
|
||||
let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
|
||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
|
||||
|
||||
before { Fabricate(:mention, account: sender, status: reply_to) }
|
||||
|
||||
it 'does not notify' do
|
||||
expect { subject }.to_not change(Notification, :count)
|
||||
end
|
||||
|
@ -77,10 +78,11 @@ RSpec.describe NotifyService, type: :service do
|
|||
|
||||
context 'when the message chain is initiated by recipient, but without a mention to the sender, even if the sender sends multiple messages in a row' do
|
||||
let(:reply_to) { Fabricate(:status, account: recipient) }
|
||||
let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
|
||||
let(:dummy_reply) { Fabricate(:status, account: sender, visibility: :direct, thread: reply_to) }
|
||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: dummy_reply)) }
|
||||
|
||||
before { Fabricate(:mention, account: sender, status: reply_to) }
|
||||
|
||||
it 'does not notify' do
|
||||
expect { subject }.to_not change(Notification, :count)
|
||||
end
|
||||
|
@ -88,9 +90,10 @@ RSpec.describe NotifyService, type: :service do
|
|||
|
||||
context 'when the message chain is initiated by the recipient with a mention to the sender' do
|
||||
let(:reply_to) { Fabricate(:status, account: recipient, visibility: :direct) }
|
||||
let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
|
||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
|
||||
|
||||
before { Fabricate(:mention, account: sender, status: reply_to) }
|
||||
|
||||
it 'does notify' do
|
||||
expect { subject }.to change(Notification, :count)
|
||||
end
|
||||
|
|
|
@ -12,17 +12,17 @@ RSpec.describe RemoveStatusService, type: :service do
|
|||
let!(:bill) { Fabricate(:account, username: 'bill', protocol: :activitypub, domain: 'example2.com', shared_inbox_url: 'http://example2.com/inbox', inbox_url: 'http://example2.com/bill/inbox') }
|
||||
|
||||
before do
|
||||
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
||||
stub_request(:post, 'http://example.com/hank/inbox').to_return(status: 200)
|
||||
stub_request(:post, 'http://example2.com/inbox').to_return(status: 200)
|
||||
stub_request(:post, 'http://example2.com/bill/inbox').to_return(status: 200)
|
||||
stub_request(:post, hank.inbox_url).to_return(status: 200)
|
||||
stub_request(:post, hank.shared_inbox_url).to_return(status: 200)
|
||||
stub_request(:post, bill.inbox_url).to_return(status: 200)
|
||||
stub_request(:post, bill.shared_inbox_url).to_return(status: 200)
|
||||
|
||||
jeff.follow!(alice)
|
||||
hank.follow!(alice)
|
||||
end
|
||||
|
||||
context 'when removed status is not a reblog' do
|
||||
let!(:status) { PostStatusService.new.call(alice, text: 'Hello @bob@example.com ThisIsASecret') }
|
||||
let!(:status) { PostStatusService.new.call(alice, text: "Hello @#{bob.pretty_acct} ThisIsASecret") }
|
||||
|
||||
before do
|
||||
FavouriteService.new.call(jeff, status)
|
||||
|
@ -41,7 +41,7 @@ RSpec.describe RemoveStatusService, type: :service do
|
|||
|
||||
it 'sends Delete activity to followers' do
|
||||
subject.call(status)
|
||||
expect(a_request(:post, 'http://example.com/inbox').with(
|
||||
expect(a_request(:post, hank.shared_inbox_url).with(
|
||||
body: hash_including({
|
||||
'type' => 'Delete',
|
||||
'object' => {
|
||||
|
@ -55,7 +55,7 @@ RSpec.describe RemoveStatusService, type: :service do
|
|||
|
||||
it 'sends Delete activity to rebloggers' do
|
||||
subject.call(status)
|
||||
expect(a_request(:post, 'http://example2.com/inbox').with(
|
||||
expect(a_request(:post, bill.shared_inbox_url).with(
|
||||
body: hash_including({
|
||||
'type' => 'Delete',
|
||||
'object' => {
|
||||
|
@ -83,7 +83,7 @@ RSpec.describe RemoveStatusService, type: :service do
|
|||
|
||||
it 'sends Delete activity to followers' do
|
||||
subject.call(status)
|
||||
expect(a_request(:post, 'http://example.com/inbox').with(
|
||||
expect(a_request(:post, hank.shared_inbox_url).with(
|
||||
body: hash_including({
|
||||
'type' => 'Delete',
|
||||
'object' => {
|
||||
|
@ -106,7 +106,7 @@ RSpec.describe RemoveStatusService, type: :service do
|
|||
|
||||
it 'sends Delete activity to conversation' do
|
||||
subject.call(status)
|
||||
expect(a_request(:post, 'http://example2.com/bill/inbox').with(
|
||||
expect(a_request(:post, bill.inbox_url).with(
|
||||
body: hash_including({
|
||||
'type' => 'Delete',
|
||||
'object' => {
|
||||
|
@ -120,8 +120,8 @@ RSpec.describe RemoveStatusService, type: :service do
|
|||
|
||||
it 'do not send Delete activity to followers' do
|
||||
subject.call(status)
|
||||
expect(a_request(:post, 'http://example.com/hank/inbox')).to_not have_been_made
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made
|
||||
expect(a_request(:post, hank.inbox_url)).to_not have_been_made
|
||||
expect(a_request(:post, hank.shared_inbox_url)).to_not have_been_made
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -131,7 +131,7 @@ RSpec.describe RemoveStatusService, type: :service do
|
|||
|
||||
it 'sends Undo activity to followers' do
|
||||
subject.call(status)
|
||||
expect(a_request(:post, 'http://example.com/inbox').with(
|
||||
expect(a_request(:post, hank.shared_inbox_url).with(
|
||||
body: hash_including({
|
||||
'type' => 'Undo',
|
||||
'object' => hash_including({
|
||||
|
@ -149,7 +149,7 @@ RSpec.describe RemoveStatusService, type: :service do
|
|||
|
||||
it 'sends Undo activity to followers' do
|
||||
subject.call(status)
|
||||
expect(a_request(:post, 'http://example.com/inbox').with(
|
||||
expect(a_request(:post, hank.shared_inbox_url).with(
|
||||
body: hash_including({
|
||||
'type' => 'Undo',
|
||||
'object' => hash_including({
|
||||
|
|
|
@ -156,9 +156,8 @@ RSpec.describe ReportService, type: :service do
|
|||
-> { described_class.new.call(source_account, target_account) }
|
||||
end
|
||||
|
||||
let!(:other_report) { Fabricate(:report, target_account: target_account) }
|
||||
|
||||
before do
|
||||
Fabricate(:report, target_account: target_account)
|
||||
ActionMailer::Base.deliveries.clear
|
||||
source_account.user.settings['notification_emails.report'] = true
|
||||
source_account.user.save
|
||||
|
|
|
@ -22,7 +22,7 @@ RSpec.describe ResolveAccountService, type: :service do
|
|||
let!(:remote_account) { Fabricate(:account, username: 'foo', domain: 'ap.example.com', protocol: 'activitypub') }
|
||||
|
||||
context 'when domain is banned' do
|
||||
let!(:domain_block) { Fabricate(:domain_block, domain: 'ap.example.com', severity: :suspend) }
|
||||
before { Fabricate(:domain_block, domain: 'ap.example.com', severity: :suspend) }
|
||||
|
||||
it 'does not return an account' do
|
||||
expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to be_nil
|
||||
|
@ -218,6 +218,7 @@ RSpec.describe ResolveAccountService, type: :service do
|
|||
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'
|
||||
expect(status.reload.account).to eq(account)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -46,9 +46,9 @@ RSpec.describe SuspendAccountService, type: :service do
|
|||
let!(:account) { Fabricate(:account) }
|
||||
let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') }
|
||||
let!(:remote_reporter) { Fabricate(:account, uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub, domain: 'bob.com') }
|
||||
let!(:report) { Fabricate(:report, account: remote_reporter, target_account: account) }
|
||||
|
||||
before do
|
||||
Fabricate(:report, account: remote_reporter, target_account: account)
|
||||
remote_follower.follow!(account)
|
||||
end
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ RSpec.describe UnallowDomainService, type: :service do
|
|||
end
|
||||
|
||||
it 'removes remote accounts from that domain' do
|
||||
expect { already_banned_account.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect(Account.where(domain: 'evil.org').exists?).to be false
|
||||
end
|
||||
|
||||
|
|
|
@ -39,9 +39,9 @@ RSpec.describe UnsuspendAccountService, type: :service do
|
|||
let!(:account) { Fabricate(:account) }
|
||||
let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') }
|
||||
let!(:remote_reporter) { Fabricate(:account, uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub, domain: 'bob.com') }
|
||||
let!(:report) { Fabricate(:report, account: remote_reporter, target_account: account) }
|
||||
|
||||
before do
|
||||
Fabricate(:report, account: remote_reporter, target_account: account)
|
||||
remote_follower.follow!(account)
|
||||
end
|
||||
|
||||
|
|
|
@ -18,12 +18,11 @@ describe Scheduler::UserCleanupScheduler do
|
|||
confirmed_user.update!(confirmed_at: 1.day.ago)
|
||||
end
|
||||
|
||||
it 'deletes the old unconfirmed user' do
|
||||
expect { subject.perform }.to change { User.exists?(old_unconfirmed_user.id) }.from(true).to(false)
|
||||
end
|
||||
|
||||
it "deletes the old unconfirmed user's account" do
|
||||
expect { subject.perform }.to change { Account.exists?(old_unconfirmed_user.account_id) }.from(true).to(false)
|
||||
it 'deletes the old unconfirmed user, their account, and the moderation note' do
|
||||
expect { subject.perform }
|
||||
.to change { User.exists?(old_unconfirmed_user.id) }.from(true).to(false)
|
||||
.and change { Account.exists?(old_unconfirmed_user.account_id) }.from(true).to(false)
|
||||
expect { moderation_note.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'does not delete the new unconfirmed user or their account' do
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue