Merge remote-tracking branch 'parent/main' into upstream-20250328

This commit is contained in:
KMY 2025-03-28 08:44:30 +09:00
commit 12ed20b6d5
257 changed files with 3505 additions and 2010 deletions

View file

@ -1,112 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ActivityPub::InboxesController do
let(:remote_account) { nil }
before do
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
describe 'POST #create' do
context 'with signature' do
let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) }
before do
post :create, body: '{}'
end
it 'returns http accepted' do
expect(response).to have_http_status(202)
end
context 'with a specific account' do
subject(:response) { post :create, params: { account_username: account.username }, body: '{}' }
let(:account) { Fabricate(:account) }
context 'when account is permanently suspended' do
before do
account.suspend!
account.deletion_request.destroy
end
it 'returns http gone' do
expect(response).to have_http_status(410)
end
end
context 'when account is temporarily suspended' do
before do
account.suspend!
end
it 'returns http accepted' do
expect(response).to have_http_status(202)
end
end
end
end
context 'with Collection-Synchronization header' do
let(:remote_account) { Fabricate(:account, followers_url: 'https://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor', protocol: :activitypub) }
let(:synchronization_collection) { remote_account.followers_url }
let(:synchronization_url) { 'https://example.com/followers-for-domain' }
let(:synchronization_hash) { 'somehash' }
let(:synchronization_header) { "collectionId=\"#{synchronization_collection}\", digest=\"#{synchronization_hash}\", url=\"#{synchronization_url}\"" }
before do
allow(ActivityPub::FollowersSynchronizationWorker).to receive(:perform_async).and_return(nil)
allow(remote_account).to receive(:local_followers_hash).and_return('somehash')
request.headers['Collection-Synchronization'] = synchronization_header
post :create, body: '{}'
end
context 'with mismatching target collection' do
let(:synchronization_collection) { 'https://example.com/followers2' }
it 'does not start a synchronization job' do
expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async)
end
end
context 'with mismatching domain in partial collection attribute' do
let(:synchronization_url) { 'https://example.org/followers' }
it 'does not start a synchronization job' do
expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async)
end
end
context 'with matching digest' do
it 'does not start a synchronization job' do
expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async)
end
end
context 'with mismatching digest' do
let(:synchronization_hash) { 'wronghash' }
it 'starts a synchronization job' do
expect(ActivityPub::FollowersSynchronizationWorker).to have_received(:perform_async)
end
end
it 'returns http accepted' do
expect(response).to have_http_status(202)
end
end
context 'without signature' do
before do
post :create, body: '{}'
end
it 'returns http not authorized' do
expect(response).to have_http_status(401)
end
end
end
end

View file

@ -1,35 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::AccountActionsController do
render_views
let(:user) { Fabricate(:admin_user) }
before do
sign_in user, scope: :user
end
describe 'GET #new' do
let(:account) { Fabricate(:account) }
it 'returns http success' do
get :new, params: { account_id: account.id }
expect(response).to have_http_status(:success)
end
end
describe 'POST #create' do
let(:account) { Fabricate(:account) }
it 'records the account action' do
expect do
post :create, params: { account_id: account.id, admin_account_action: { type: 'silence' } }
end.to change { account.strikes.count }.by(1)
expect(response).to redirect_to(admin_account_path(account.id))
end
end
end

View file

@ -1,48 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::ChangeEmailsController do
render_views
let(:admin) { Fabricate(:admin_user) }
before do
sign_in admin
end
describe 'GET #show' do
it 'returns http success' do
user = Fabricate(:user)
get :show, params: { account_id: user.account.id }
expect(response).to have_http_status(200)
end
end
describe 'GET #update' do
before do
allow(UserMailer).to receive(:confirmation_instructions)
.and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil))
end
it 'returns http success' do
user = Fabricate(:user)
previous_email = user.email
post :update, params: { account_id: user.account.id, user: { unconfirmed_email: 'test@example.com' } }
user.reload
expect(user.email).to eq previous_email
expect(user.unconfirmed_email).to eq 'test@example.com'
expect(user.confirmation_token).to_not be_nil
expect(UserMailer).to have_received(:confirmation_instructions).with(user, user.confirmation_token, { to: 'test@example.com' })
expect(response).to redirect_to(admin_account_path(user.account.id))
end
end
end

View file

@ -1,54 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
require 'webauthn/fake_client'
RSpec.describe Admin::Users::TwoFactorAuthenticationsController do
render_views
let(:user) { Fabricate(:user) }
before do
sign_in Fabricate(:admin_user), scope: :user
end
describe 'DELETE #destroy' do
context 'when user has OTP enabled' do
before do
user.update(otp_required_for_login: true)
end
it 'redirects to admin account page' do
delete :destroy, params: { user_id: user.id }
user.reload
expect(user.otp_enabled?).to be false
expect(response).to redirect_to(admin_account_path(user.account_id))
end
end
context 'when user has OTP and WebAuthn enabled' do
let(:fake_client) { WebAuthn::FakeClient.new('http://test.host') }
before do
user.update(otp_required_for_login: true, webauthn_id: WebAuthn.generate_user_id)
public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
Fabricate(:webauthn_credential,
user_id: user.id,
external_id: public_key_credential.id,
public_key: public_key_credential.public_key,
nickname: 'Security Key')
end
it 'redirects to admin account page' do
delete :destroy, params: { user_id: user.id }
user.reload
expect(user.otp_enabled?).to be false
expect(user.webauthn_enabled?).to be false
expect(response).to redirect_to(admin_account_path(user.account_id))
end
end
end
end

View file

@ -342,6 +342,42 @@ RSpec.describe Auth::RegistrationsController do
end
end
context 'when age verification is enabled' do
subject { post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' }.merge(date_of_birth) } }
before do
Setting.min_age = 16
end
let(:date_of_birth) { {} }
context 'when date of birth is below age limit' do
let(:date_of_birth) { 13.years.ago.then { |date| { 'date_of_birth(1i)': date.day.to_s, 'date_of_birth(2i)': date.month.to_s, 'date_of_birth(3i)': date.year.to_s } } }
it 'does not create user' do
subject
user = User.find_by(email: 'test@example.com')
expect(user).to be_nil
end
end
context 'when date of birth is above age limit' do
let(:date_of_birth) { 17.years.ago.then { |date| { 'date_of_birth(1i)': date.day.to_s, 'date_of_birth(2i)': date.month.to_s, 'date_of_birth(3i)': date.year.to_s } } }
it 'redirects to setup and creates user' do
subject
expect(response).to redirect_to auth_setup_path
expect(User.find_by(email: 'test@example.com'))
.to be_present
.and have_attributes(
age_verified_at: not_eq(nil)
)
end
end
end
context 'when max user count is set' do
subject do
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' } }

View file

@ -1,32 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Disputes::StrikesController do
render_views
before { sign_in current_user, scope: :user }
describe '#show' do
let(:current_user) { Fabricate(:user) }
let(:strike) { Fabricate(:account_warning, target_account: current_user.account) }
before do
get :show, params: { id: strike.id }
end
context 'when meant for the user' do
it 'returns http success' do
expect(response).to have_http_status(:success)
end
end
context 'when meant for a different user' do
let(:strike) { Fabricate(:account_warning) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
end
end

View file

@ -1,45 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Filters::StatusesController do
render_views
describe 'GET #index' do
let(:filter) { Fabricate(:custom_filter) }
context 'with signed out user' do
it 'redirects' do
get :index, params: { filter_id: filter }
expect(response).to be_redirect
end
end
context 'with a signed in user' do
context 'with the filter user signed in' do
before do
sign_in(filter.account.user)
get :index, params: { filter_id: filter }
end
it 'returns http success and private cache control headers' do
expect(response).to have_http_status(200)
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end
context 'with another user signed in' do
before do
sign_in(Fabricate(:user))
get :index, params: { filter_id: filter }
end
it 'returns http not found' do
expect(response).to have_http_status(404)
end
end
end
end
end

View file

@ -1,23 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Oauth::TokensController do
describe 'POST #revoke' do
let!(:user) { Fabricate(:user) }
let!(:application) { Fabricate(:application, confidential: false) }
let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: application) }
let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) }
it 'revokes the token and removes subscriptions' do
post :revoke, params: { client_id: application.uid, token: access_token.token }
expect(access_token.reload.revoked_at)
.to_not be_nil
expect(Web::PushSubscription.where(access_token: access_token).count)
.to eq(0)
expect { web_push_subscription.reload }
.to raise_error(ActiveRecord::RecordNotFound)
end
end
end

View file

@ -1,87 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Settings::DeletesController do
render_views
describe 'GET #show' do
context 'when signed in' do
let(:user) { Fabricate(:user) }
before do
sign_in user, scope: :user
get :show
end
it 'renders confirmation page with private cache control headers', :aggregate_failures do
expect(response).to have_http_status(200)
expect(response.headers['Cache-Control']).to include('private, no-store')
end
context 'when suspended' do
let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
it 'returns http forbidden with private cache control headers', :aggregate_failures do
expect(response).to have_http_status(403)
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end
end
context 'when not signed in' do
it 'redirects' do
get :show
expect(response).to redirect_to '/auth/sign_in'
end
end
end
describe 'DELETE #destroy' do
context 'when signed in' do
let(:user) { Fabricate(:user, password: 'petsmoldoggos') }
before do
sign_in user, scope: :user
end
context 'with correct password' do
before do
delete :destroy, params: { form_delete_confirmation: { password: 'petsmoldoggos' } }
end
it 'removes user record and redirects', :aggregate_failures, :inline_jobs do
expect(response).to redirect_to '/auth/sign_in'
expect(User.find_by(id: user.id)).to be_nil
expect(user.account.reload).to be_suspended
expect(CanonicalEmailBlock.block?(user.email)).to be false
end
context 'when suspended' do
let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
end
context 'with incorrect password' do
before do
delete :destroy, params: { form_delete_confirmation: { password: 'blaze420' } }
end
it 'redirects back to confirmation page' do
expect(response).to redirect_to settings_delete_path
end
end
end
context 'when not signed in' do
it 'redirects' do
delete :destroy
expect(response).to redirect_to '/auth/sign_in'
end
end
end
end

View file

@ -0,0 +1,32 @@
# frozen_string_literal: true
require 'rails_helper'
require 'prometheus_exporter'
require 'prometheus_exporter/middleware'
require 'mastodon/middleware/prometheus_queue_time'
RSpec.describe Mastodon::Middleware::PrometheusQueueTime do
subject { described_class.new(app, client:) }
let(:app) do
proc { |_env| [200, {}, 'OK'] }
end
let(:client) do
instance_double(PrometheusExporter::Client, send_json: true)
end
describe '#call' do
let(:env) do
{
'HTTP_X_REQUEST_START' => "t=#{(Time.now.to_f * 1000).to_i}",
}
end
it 'reports a queue time to the client' do
subject.call(env)
expect(client).to have_received(:send_json)
.with(hash_including(queue_time: instance_of(Float)))
end
end
end

View file

@ -295,12 +295,21 @@ RSpec.describe MediaAttachment, :attachment_processing do
end
it 'queues CacheBusterWorker jobs' do
original_path = media.file.path(:original)
small_path = media.file.path(:small)
original_url = media.file.url(:original)
small_url = media.file.url(:small)
expect { media.destroy }
.to enqueue_sidekiq_job(CacheBusterWorker).with(original_path)
.and enqueue_sidekiq_job(CacheBusterWorker).with(small_path)
.to enqueue_sidekiq_job(CacheBusterWorker).with(original_url)
.and enqueue_sidekiq_job(CacheBusterWorker).with(small_url)
end
context 'with a missing remote attachment' do
let(:media) { Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png', file: nil) }
it 'does not queue CacheBusterWorker jobs' do
expect { media.destroy }
.to_not enqueue_sidekiq_job(CacheBusterWorker)
end
end
end

View file

@ -2,22 +2,19 @@
require 'rails_helper'
RSpec.describe ActivityPub::CollectionsController do
RSpec.describe 'ActivityPub Collections' do
let!(:account) { Fabricate(:account) }
let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) }
let(:remote_account) { nil }
before do
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
Fabricate(:status_pin, account: account)
Fabricate(:status_pin, account: account)
Fabricate.times(2, :status_pin, account: account)
Fabricate(:status_pin, account: account, status: private_pinned)
Fabricate(:status, account: account, visibility: :private)
end
describe 'GET #show' do
subject(:response) { get :show, params: { id: id, account_username: account.username } }
subject { get account_collection_path(id: id, account_username: account.username), headers: nil, sign_with: remote_account }
context 'when id is "featured"' do
let(:id) { 'featured' }
@ -26,10 +23,13 @@ RSpec.describe ActivityPub::CollectionsController do
let(:remote_account) { nil }
it 'returns http success and correct media type and correct items' do
subject
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json'
expect(response.media_type)
.to eq 'application/activity+json'
expect(response.parsed_body[:orderedItems])
.to be_an(Array)
@ -45,17 +45,21 @@ RSpec.describe ActivityPub::CollectionsController do
end
it 'returns http gone' do
expect(response).to have_http_status(410)
subject
expect(response)
.to have_http_status(410)
end
end
context 'when account is temporarily suspended' do
before do
account.suspend!
end
before { account.suspend! }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
subject
expect(response)
.to have_http_status(403)
end
end
end
@ -65,11 +69,14 @@ RSpec.describe ActivityPub::CollectionsController do
context 'when getting a featured resource' do
it 'returns http success and correct media type and expected items' do
subject
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json'
expect(response.media_type)
.to eq 'application/activity+json'
expect(response.parsed_body[:orderedItems])
.to be_an(Array)
@ -80,39 +87,45 @@ RSpec.describe ActivityPub::CollectionsController do
end
context 'with authorized fetch mode' do
before do
allow(controller).to receive(:authorized_fetch_mode?).and_return(true)
end
before { Setting.authorized_fetch = true }
context 'when signed request account is blocked' do
before do
account.block!(remote_account)
end
before { account.block!(remote_account) }
it 'returns http success and correct media type and cache headers and empty items' do
expect(response).to have_http_status(200)
expect(response.media_type).to eq 'application/activity+json'
expect(response.headers['Cache-Control']).to include 'private'
subject
expect(response.parsed_body[:orderedItems])
.to be_an(Array)
.and be_empty
expect(response)
.to have_http_status(200)
expect(response.media_type)
.to eq('application/activity+json')
expect(response.headers['Cache-Control'])
.to include('private')
expect(response.parsed_body)
.to include(
orderedItems: be_an(Array).and(be_empty)
)
end
end
context 'when signed request account is domain blocked' do
before do
account.block_domain!(remote_account.domain)
end
before { account.block_domain!(remote_account.domain) }
it 'returns http success and correct media type and cache headers and empty items' do
expect(response).to have_http_status(200)
expect(response.media_type).to eq 'application/activity+json'
expect(response.headers['Cache-Control']).to include 'private'
subject
expect(response.parsed_body[:orderedItems])
.to be_an(Array)
.and be_empty
expect(response)
.to have_http_status(200)
expect(response.media_type)
.to eq('application/activity+json')
expect(response.headers['Cache-Control'])
.to include('private')
expect(response.parsed_body)
.to include(
orderedItems: be_an(Array).and(be_empty)
)
end
end
end
@ -123,7 +136,10 @@ RSpec.describe ActivityPub::CollectionsController do
let(:id) { 'hoge' }
it 'returns http not found' do
expect(response).to have_http_status(404)
subject
expect(response)
.to have_http_status(404)
end
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
RSpec.describe ActivityPub::FollowersSynchronizationsController do
RSpec.describe 'ActivityPub Follower Synchronizations' do
let!(:account) { Fabricate(:account) }
let!(:follower_example_com_user_a) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/a') }
let!(:follower_example_com_user_b) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/b') }
@ -14,32 +14,34 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController do
follower_example_com_user_b.follow!(account)
follower_foo_com_user_a.follow!(account)
follower_example_com_instance_actor.follow!(account)
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
describe 'GET #show' do
context 'without signature' do
let(:remote_account) { nil }
before do
get :show, params: { account_username: account.username }
end
subject { get account_followers_synchronization_path(account_username: account.username) }
it 'returns http not authorized' do
expect(response).to have_http_status(401)
subject
expect(response)
.to have_http_status(401)
end
end
context 'with signature from example.com' do
subject(:response) { get :show, params: { account_username: account.username } }
subject { get account_followers_synchronization_path(account_username: account.username), headers: nil, sign_with: remote_account }
let(:remote_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/instance') }
it 'returns http success and cache control and activity json types and correct items' do
expect(response).to have_http_status(200)
expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
expect(response.media_type).to eq 'application/activity+json'
subject
expect(response)
.to have_http_status(200)
expect(response.headers['Cache-Control'])
.to eq 'max-age=0, private'
expect(response.media_type)
.to eq 'application/activity+json'
expect(response.parsed_body[:orderedItems])
.to be_an(Array)
@ -57,17 +59,21 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController do
end
it 'returns http gone' do
expect(response).to have_http_status(410)
subject
expect(response)
.to have_http_status(410)
end
end
context 'when account is temporarily suspended' do
before do
account.suspend!
end
before { account.suspend! }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
subject
expect(response)
.to have_http_status(403)
end
end
end

View file

@ -0,0 +1,148 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'ActivityPub Inboxes' do
let(:remote_account) { nil }
describe 'POST #create' do
context 'with signature' do
let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) }
context 'without a named account' do
subject { post inbox_path, params: {}.to_json, sign_with: remote_account }
it 'returns http accepted' do
subject
expect(response)
.to have_http_status(202)
end
end
context 'with a specific account' do
subject { post account_inbox_path(account_username: account.username), params: {}.to_json, sign_with: remote_account }
let(:account) { Fabricate(:account) }
context 'when account is permanently suspended' do
before do
account.suspend!
account.deletion_request.destroy
end
it 'returns http gone' do
subject
expect(response)
.to have_http_status(410)
end
end
context 'when account is temporarily suspended' do
before { account.suspend! }
it 'returns http accepted' do
subject
expect(response)
.to have_http_status(202)
end
end
end
end
context 'with Collection-Synchronization header' do
subject { post inbox_path, params: {}.to_json, headers: { 'Collection-Synchronization' => synchronization_header }, sign_with: remote_account }
let(:remote_account) { Fabricate(:account, followers_url: 'https://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor', protocol: :activitypub) }
let(:synchronization_collection) { remote_account.followers_url }
let(:synchronization_url) { 'https://example.com/followers-for-domain' }
let(:synchronization_hash) { 'somehash' }
let(:synchronization_header) { "collectionId=\"#{synchronization_collection}\", digest=\"#{synchronization_hash}\", url=\"#{synchronization_url}\"" }
before do
stub_follow_sync_worker
stub_followers_hash
end
context 'with mismatching target collection' do
let(:synchronization_collection) { 'https://example.com/followers2' }
it 'does not start a synchronization job' do
subject
expect(response)
.to have_http_status(202)
expect(ActivityPub::FollowersSynchronizationWorker)
.to_not have_received(:perform_async)
end
end
context 'with mismatching domain in partial collection attribute' do
let(:synchronization_url) { 'https://example.org/followers' }
it 'does not start a synchronization job' do
subject
expect(response)
.to have_http_status(202)
expect(ActivityPub::FollowersSynchronizationWorker)
.to_not have_received(:perform_async)
end
end
context 'with matching digest' do
it 'does not start a synchronization job' do
subject
expect(response)
.to have_http_status(202)
expect(ActivityPub::FollowersSynchronizationWorker)
.to_not have_received(:perform_async)
end
end
context 'with mismatching digest' do
let(:synchronization_hash) { 'wronghash' }
it 'starts a synchronization job' do
subject
expect(response)
.to have_http_status(202)
expect(ActivityPub::FollowersSynchronizationWorker)
.to have_received(:perform_async)
end
end
it 'returns http accepted' do
subject
expect(response)
.to have_http_status(202)
end
def stub_follow_sync_worker
allow(ActivityPub::FollowersSynchronizationWorker)
.to receive(:perform_async)
.and_return(nil)
end
def stub_followers_hash
Rails.cache.write("followers_hash:#{remote_account.id}:local", 'somehash') # Populate value to match request
end
end
context 'without signature' do
subject { post inbox_path, params: {}.to_json }
it 'returns http not authorized' do
subject
expect(response)
.to have_http_status(401)
end
end
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
RSpec.describe ActivityPub::OutboxesController do
RSpec.describe 'ActivityPub Outboxes' do
let!(:account) { Fabricate(:account) }
before do
@ -11,13 +11,11 @@ RSpec.describe ActivityPub::OutboxesController do
Fabricate(:status, account: account, visibility: :private)
Fabricate(:status, account: account, visibility: :direct)
Fabricate(:status, account: account, visibility: :limited)
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
describe 'GET #show' do
context 'without signature' do
subject(:response) { get :show, params: { account_username: account.username, page: page } }
subject { get account_outbox_path(account_username: account.username, page: page) }
let(:remote_account) { nil }
@ -25,13 +23,18 @@ RSpec.describe ActivityPub::OutboxesController do
let(:page) { nil }
it 'returns http success and correct media type and headers and items count' do
subject
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json'
expect(response.headers['Vary']).to be_nil
expect(response.parsed_body[:totalItems]).to eq 4
expect(response.media_type)
.to eq 'application/activity+json'
expect(response.headers['Vary'])
.to be_nil
expect(response.parsed_body[:totalItems])
.to eq 4
end
context 'when account is permanently suspended' do
@ -41,17 +44,21 @@ RSpec.describe ActivityPub::OutboxesController do
end
it 'returns http gone' do
expect(response).to have_http_status(410)
subject
expect(response)
.to have_http_status(410)
end
end
context 'when account is temporarily suspended' do
before do
account.suspend!
end
before { account.suspend! }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
subject
expect(response)
.to have_http_status(403)
end
end
end
@ -60,12 +67,16 @@ RSpec.describe ActivityPub::OutboxesController do
let(:page) { 'true' }
it 'returns http success and correct media type and vary header and items' do
subject
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json'
expect(response.headers['Vary']).to include 'Signature'
expect(response.media_type)
.to eq 'application/activity+json'
expect(response.headers['Vary'])
.to include 'Signature'
expect(response.parsed_body)
.to include(
@ -82,35 +93,42 @@ RSpec.describe ActivityPub::OutboxesController do
end
it 'returns http gone' do
expect(response).to have_http_status(410)
subject
expect(response)
.to have_http_status(410)
end
end
context 'when account is temporarily suspended' do
before do
account.suspend!
end
before { account.suspend! }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
subject
expect(response)
.to have_http_status(403)
end
end
end
end
context 'with signature' do
subject { get account_outbox_path(account_username: account.username, page: page), headers: nil, sign_with: remote_account }
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
let(:page) { 'true' }
context 'when signed request account does not follow account' do
before do
get :show, params: { account_username: account.username, page: page }
end
it 'returns http success and correct media type and headers and items' do
expect(response).to have_http_status(200)
expect(response.media_type).to eq 'application/activity+json'
expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
subject
expect(response)
.to have_http_status(200)
expect(response.media_type)
.to eq 'application/activity+json'
expect(response.headers['Cache-Control'])
.to eq 'private, no-store'
expect(response.parsed_body)
.to include(
@ -122,15 +140,17 @@ RSpec.describe ActivityPub::OutboxesController do
end
context 'when signed request account follows account' do
before do
remote_account.follow!(account)
get :show, params: { account_username: account.username, page: page }
end
before { remote_account.follow!(account) }
it 'returns http success and correct media type and headers and items' do
expect(response).to have_http_status(200)
expect(response.media_type).to eq 'application/activity+json'
expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
subject
expect(response)
.to have_http_status(200)
expect(response.media_type)
.to eq 'application/activity+json'
expect(response.headers['Cache-Control'])
.to eq 'private, no-store'
expect(response.parsed_body)
.to include(
@ -142,15 +162,17 @@ RSpec.describe ActivityPub::OutboxesController do
end
context 'when signed request account is blocked' do
before do
account.block!(remote_account)
get :show, params: { account_username: account.username, page: page }
end
before { account.block!(remote_account) }
it 'returns http success and correct media type and headers and items' do
expect(response).to have_http_status(200)
expect(response.media_type).to eq 'application/activity+json'
expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
subject
expect(response)
.to have_http_status(200)
expect(response.media_type)
.to eq 'application/activity+json'
expect(response.headers['Cache-Control'])
.to eq 'private, no-store'
expect(response.parsed_body)
.to include(
@ -160,15 +182,17 @@ RSpec.describe ActivityPub::OutboxesController do
end
context 'when signed request account is domain blocked' do
before do
account.block_domain!(remote_account.domain)
get :show, params: { account_username: account.username, page: page }
end
before { account.block_domain!(remote_account.domain) }
it 'returns http success and correct media type and headers and items' do
expect(response).to have_http_status(200)
expect(response.media_type).to eq 'application/activity+json'
expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
subject
expect(response)
.to have_http_status(200)
expect(response.media_type)
.to eq 'application/activity+json'
expect(response.headers['Cache-Control'])
.to eq 'private, no-store'
expect(response.parsed_body)
.to include(

View file

@ -2,9 +2,9 @@
require 'rails_helper'
RSpec.describe ActivityPub::RepliesController do
RSpec.describe 'ActivityPub Replies' do
let(:status) { Fabricate(:status, visibility: parent_visibility) }
let(:remote_account) { Fabricate(:account, domain: 'foobar.com') }
let(:remote_account) { Fabricate(:account, domain: 'foobar.com') }
let(:remote_reply_id) { 'https://foobar.com/statuses/1234' }
let(:remote_querier) { nil }
@ -13,7 +13,10 @@ RSpec.describe ActivityPub::RepliesController do
let(:parent_visibility) { :private }
it 'returns http not found' do
expect(response).to have_http_status(404)
subject
expect(response)
.to have_http_status(404)
end
end
@ -21,7 +24,10 @@ RSpec.describe ActivityPub::RepliesController do
let(:parent_visibility) { :direct }
it 'returns http not found' do
expect(response).to have_http_status(404)
subject
expect(response)
.to have_http_status(404)
end
end
end
@ -31,7 +37,10 @@ RSpec.describe ActivityPub::RepliesController do
let(:parent_visibility) { :public }
it 'returns http not found' do
expect(response).to have_http_status(404)
subject
expect(response)
.to have_http_status(404)
end
end
@ -48,19 +57,23 @@ RSpec.describe ActivityPub::RepliesController do
end
it 'returns http gone' do
expect(response).to have_http_status(410)
subject
expect(response)
.to have_http_status(410)
end
end
context 'when account is temporarily suspended' do
let(:parent_visibility) { :public }
before do
status.account.suspend!
end
before { status.account.suspend! }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
subject
expect(response)
.to have_http_status(403)
end
end
@ -68,15 +81,20 @@ RSpec.describe ActivityPub::RepliesController do
let(:parent_visibility) { :public }
it 'returns http success and correct media type' do
subject
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json'
expect(response.media_type)
.to eq 'application/activity+json'
end
context 'without only_other_accounts' do
context 'without `only_other_accounts` param' do
it "returns items with thread author's replies" do
subject
expect(response.parsed_body)
.to include(
first: be_a(Hash).and(
@ -91,6 +109,8 @@ RSpec.describe ActivityPub::RepliesController do
context 'when there are few self-replies' do
it 'points next to replies from other people' do
subject
expect(response.parsed_body)
.to include(
first: be_a(Hash).and(
@ -108,6 +128,8 @@ RSpec.describe ActivityPub::RepliesController do
end
it 'points next to other self-replies' do
subject
expect(response.parsed_body)
.to include(
first: be_a(Hash).and(
@ -120,31 +142,33 @@ RSpec.describe ActivityPub::RepliesController do
end
end
context 'with only_other_accounts' do
context 'with `only_other_accounts` param' do
let(:only_other_accounts) { 'true' }
it 'returns items with other public or unlisted replies' do
it 'returns items with other public or unlisted replies and correctly inlines replies and uses IDs' do
subject
expect(response.parsed_body)
.to include(
first: be_a(Hash).and(
include(items: be_an(Array).and(have_attributes(size: 3)))
)
)
end
it 'only inlines items that are local and public or unlisted replies' do
# Only inline replies that are local and public, or unlisted
expect(inlined_replies)
.to all(satisfy { |item| targets_public_collection?(item) })
.and all(satisfy { |item| ActivityPub::TagManager.instance.local_uri?(item[:id]) })
end
it 'uses ids for remote toots' do
# Use ids for remote replies
expect(remote_replies)
.to all(satisfy { |item| item.is_a?(String) && !ActivityPub::TagManager.instance.local_uri?(item) })
end
context 'when there are few replies' do
it 'does not have a next page' do
subject
expect(response.parsed_body)
.to include(
first: be_a(Hash).and(not_include(next: be_present))
@ -158,6 +182,8 @@ RSpec.describe ActivityPub::RepliesController do
end
it 'points next to other replies' do
subject
expect(response.parsed_body)
.to include(
first: be_a(Hash).and(
@ -176,10 +202,8 @@ RSpec.describe ActivityPub::RepliesController do
before do
stub_const 'ActivityPub::RepliesController::DESCENDANTS_LIMIT', 5
allow(controller).to receive(:signed_request_actor).and_return(remote_querier)
Fabricate(:status, thread: status, visibility: :public)
Fabricate(:status, thread: status, visibility: :public)
Fabricate.times(2, :status, thread: status, visibility: :public)
Fabricate(:status, thread: status, visibility: :private)
Fabricate(:status, account: status.account, thread: status, visibility: :public)
Fabricate(:status, account: status.account, thread: status, visibility: :private)
@ -188,31 +212,29 @@ RSpec.describe ActivityPub::RepliesController do
end
describe 'GET #index' do
subject(:response) { get :index, params: { account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts } }
let(:only_other_accounts) { nil }
context 'with no signature' do
subject { get account_status_replies_path(account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts) }
it_behaves_like 'allowed access'
end
context 'with signature' do
subject { get account_status_replies_path(account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts), headers: nil, sign_with: remote_querier }
let(:remote_querier) { Fabricate(:account, domain: 'example.com') }
it_behaves_like 'allowed access'
context 'when signed request account is blocked' do
before do
status.account.block!(remote_querier)
end
before { status.account.block!(remote_querier) }
it_behaves_like 'disallowed access'
end
context 'when signed request account is domain blocked' do
before do
status.account.block_domain!(remote_querier.domain)
end
before { status.account.block_domain!(remote_querier.domain) }
it_behaves_like 'disallowed access'
end

View file

@ -74,12 +74,45 @@ RSpec.describe '/api/v1/accounts' do
describe 'POST /api/v1/accounts' do
subject do
post '/api/v1/accounts', headers: headers, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement }
post '/api/v1/accounts', headers: headers, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement, date_of_birth: date_of_birth }
end
let(:client_app) { Fabricate(:application) }
let(:token) { Doorkeeper::AccessToken.find_or_create_for(application: client_app, resource_owner: nil, scopes: 'read write', use_refresh_token: false) }
let(:agreement) { nil }
let(:date_of_birth) { nil }
context 'when age verification is enabled' do
before do
Setting.min_age = 16
end
let(:agreement) { 'true' }
context 'when date of birth is below age limit' do
let(:date_of_birth) { 13.years.ago.strftime('%d.%m.%Y') }
it 'returns http unprocessable entity' do
subject
expect(response).to have_http_status(422)
expect(response.content_type)
.to start_with('application/json')
end
end
context 'when date of birth is over age limit' do
let(:date_of_birth) { 17.years.ago.strftime('%d.%m.%Y') }
it 'creates a user', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
end
end
end
context 'when given truthy agreement' do
let(:agreement) { 'true' }

View file

@ -15,6 +15,8 @@ RSpec.describe 'API V1 Trends Tags' do
.and not_have_http_link_header
expect(response.content_type)
.to start_with('application/json')
expect(response.headers['Deprecation'])
.to be_nil
end
end
@ -31,6 +33,8 @@ RSpec.describe 'API V1 Trends Tags' do
.and have_http_link_header(api_v1_trends_tags_url(offset: 2)).for(rel: 'next')
expect(response.content_type)
.to start_with('application/json')
expect(response.headers['Deprecation'])
.to be_nil
end
def prepare_trends

View file

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'deprecated API V1 Trends Tags' do
describe 'GET /api/v1/trends' do
context 'when trends are disabled' do
before { Setting.trends = false }
it 'returns http success' do
get '/api/v1/trends'
expect(response)
.to have_http_status(200)
.and not_have_http_link_header
expect(response.content_type)
.to start_with('application/json')
expect(response.headers['Deprecation'])
.to start_with '@'
end
end
context 'when trends are enabled' do
before { Setting.trends = true }
it 'returns http success' do
prepare_trends
stub_const('Api::V1::Trends::TagsController::DEFAULT_TAGS_LIMIT', 2)
get '/api/v1/trends'
expect(response)
.to have_http_status(200)
.and have_http_link_header(api_v1_trends_tags_url(offset: 2)).for(rel: 'next')
expect(response.content_type)
.to start_with('application/json')
expect(response.headers['Deprecation'])
.to start_with '@'
end
def prepare_trends
Fabricate.times(3, :tag, trendable: true).each do |tag|
2.times { |i| Trends.tags.add(tag, i) }
end
Trends::Tags.new(threshold: 1).refresh
end
end
end
end

View file

@ -158,19 +158,18 @@ RSpec.describe 'Notifications' do
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body[:notification_groups]).to contain_exactly(
a_hash_including(
type: 'favourite',
sample_account_ids: have_attributes(size: 5),
page_min_id: notification_ids.first.to_s,
page_max_id: notification_ids.last.to_s
)
)
expect(response.parsed_body[:notification_groups].size)
.to eq(1)
expect(response.parsed_body.dig(:notification_groups, 0))
.to include(type: 'favourite')
.and(include(sample_account_ids: have_attributes(size: 5)))
.and(include(page_max_id: notification_ids.last.to_s))
.and(include(page_min_id: notification_ids.first.to_s))
end
end
context 'with min_id param' do
let(:params) { { min_id: user.account.notifications.reload.first.id - 1 } }
let(:params) { { min_id: user.account.notifications.order(id: :asc).first.id - 1 } }
it 'returns a notification group covering all notifications' do
subject
@ -180,14 +179,13 @@ RSpec.describe 'Notifications' do
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body[:notification_groups]).to contain_exactly(
a_hash_including(
type: 'favourite',
sample_account_ids: have_attributes(size: 5),
page_min_id: notification_ids.first.to_s,
page_max_id: notification_ids.last.to_s
)
)
expect(response.parsed_body[:notification_groups].size)
.to eq(1)
expect(response.parsed_body.dig(:notification_groups, 0))
.to include(type: 'favourite')
.and(include(sample_account_ids: have_attributes(size: 5)))
.and(include(page_max_id: notification_ids.last.to_s))
.and(include(page_min_id: notification_ids.first.to_s))
end
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Disputes Strikes' do
before { sign_in current_user }
describe 'GET /disputes/strikes/:id' do
let(:current_user) { Fabricate(:user) }
context 'when meant for a different user' do
let(:strike) { Fabricate(:account_warning) }
it 'returns http forbidden' do
get disputes_strike_path(strike)
expect(response)
.to have_http_status(403)
end
end
end
end

View file

@ -16,4 +16,30 @@ RSpec.describe 'Filters Statuses' do
.to redirect_to(edit_filter_path(filter))
end
end
describe 'GET /filters/:filter_id/statuses' do
let(:filter) { Fabricate(:custom_filter) }
context 'with signed out user' do
it 'redirects' do
get filter_statuses_path(filter)
expect(response)
.to be_redirect
end
end
context 'with a signed in user' do
context 'with another user signed in' do
before { sign_in(Fabricate(:user)) }
it 'returns http not found' do
get filter_statuses_path(filter)
expect(response)
.to have_http_status(404)
end
end
end
end
end

View file

@ -2,15 +2,15 @@
require 'rails_helper'
RSpec.describe IntentsController do
render_views
RSpec.describe 'Intents' do
let(:user) { Fabricate(:user) }
before { sign_in user, scope: :user }
describe 'GET #show' do
subject { get :show, params: { uri: uri } }
describe 'GET /intent' do
subject { response }
before { get intent_path(uri: uri) }
context 'when schema is web+mastodon' do
context 'when host is follow' do

View file

@ -2,7 +2,7 @@
require 'rails_helper'
RSpec.describe 'Obtaining OAuth Tokens' do
RSpec.describe 'Managing OAuth Tokens' do
describe 'POST /oauth/token' do
subject do
post '/oauth/token', params: params
@ -104,4 +104,23 @@ RSpec.describe 'Obtaining OAuth Tokens' do
end
end
end
describe 'POST /oauth/revoke' do
subject { post '/oauth/revoke', params: { client_id: application.uid, token: access_token.token } }
let!(:user) { Fabricate(:user) }
let!(:application) { Fabricate(:application, confidential: false) }
let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: application) }
let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) }
it 'revokes the token and removes subscriptions' do
expect { subject }
.to change { access_token.reload.revoked_at }.from(nil).to(be_present)
expect(Web::PushSubscription.where(access_token: access_token).count)
.to eq(0)
expect { web_push_subscription.reload }
.to raise_error(ActiveRecord::RecordNotFound)
end
end
end

View file

@ -4,13 +4,65 @@ require 'rails_helper'
RSpec.describe 'Settings Deletes' do
describe 'DELETE /settings/delete' do
before { sign_in Fabricate(:user) }
context 'when signed in' do
before { sign_in(user) }
it 'gracefully handles invalid nested params' do
delete settings_delete_path(form_delete_confirmation: 'invalid')
let(:user) { Fabricate(:user) }
expect(response)
.to have_http_status(400)
it 'gracefully handles invalid nested params' do
delete settings_delete_path(form_delete_confirmation: 'invalid')
expect(response)
.to have_http_status(400)
end
context 'when suspended' do
let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
it 'returns http forbidden' do
delete settings_delete_path
expect(response)
.to have_http_status(403)
end
end
end
context 'when not signed in' do
it 'redirects to sign in' do
delete settings_delete_path
expect(response)
.to redirect_to(new_user_session_path)
end
end
end
describe 'GET /settings/delete' do
context 'when signed in' do
before { sign_in(user) }
context 'when suspended' do
let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) }
it 'returns http forbidden with private cache control headers' do
get settings_delete_path
expect(response)
.to have_http_status(403)
expect(response.headers['Cache-Control'])
.to include('private, no-store')
end
end
end
context 'when not signed in' do
it 'redirects to sign in' do
get settings_delete_path
expect(response)
.to redirect_to(new_user_session_path)
end
end
end
end

View file

@ -10,7 +10,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
let(:bob) { Fabricate(:account, username: 'bob') }
let(:eve) { Fabricate(:account, username: 'eve') }
let(:mallory) { Fabricate(:account, username: 'mallory') }
let(:collection_uri) { 'http://example.com/partial-followers' }
let(:collection_uri) { 'https://example.com/partial-followers' }
let(:items) do
[alice, eve, mallory].map do |account|
@ -27,14 +27,14 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
}.with_indifferent_access
end
before do
alice.follow!(actor)
bob.follow!(actor)
mallory.request_follow!(actor)
end
shared_examples 'synchronizes followers' do
before do
alice.follow!(actor)
bob.follow!(actor)
mallory.request_follow!(actor)
allow(ActivityPub::DeliveryWorker).to receive(:perform_async)
subject.call(actor, collection_uri)
end
@ -46,7 +46,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
expect(mallory)
.to be_following(actor) # Convert follow request to follow when accepted
expect(ActivityPub::DeliveryWorker)
.to have_received(:perform_async).with(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor
.to have_enqueued_sidekiq_job(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor
end
end
@ -76,7 +76,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
it_behaves_like 'synchronizes followers'
end
context 'when the endpoint is a paginated Collection of actor URIs' do
context 'when the endpoint is a single-page paginated Collection of actor URIs' do
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
@ -96,5 +96,106 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
it_behaves_like 'synchronizes followers'
end
context 'when the endpoint is a paginated Collection of actor URIs split across multiple pages' do
before do
stub_request(:get, 'https://example.com/partial-followers')
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Collection',
id: 'https://example.com/partial-followers',
first: 'https://example.com/partial-followers/1',
}))
stub_request(:get, 'https://example.com/partial-followers/1')
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'CollectionPage',
id: 'https://example.com/partial-followers/1',
partOf: 'https://example.com/partial-followers',
next: 'https://example.com/partial-followers/2',
items: [alice, eve].map { |account| ActivityPub::TagManager.instance.uri_for(account) },
}))
stub_request(:get, 'https://example.com/partial-followers/2')
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'CollectionPage',
id: 'https://example.com/partial-followers/2',
partOf: 'https://example.com/partial-followers',
items: ActivityPub::TagManager.instance.uri_for(mallory),
}))
end
it_behaves_like 'synchronizes followers'
end
context 'when the endpoint is a paginated Collection of actor URIs split across, but one page errors out' do
before do
stub_request(:get, 'https://example.com/partial-followers')
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Collection',
id: 'https://example.com/partial-followers',
first: 'https://example.com/partial-followers/1',
}))
stub_request(:get, 'https://example.com/partial-followers/1')
.to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'CollectionPage',
id: 'https://example.com/partial-followers/1',
partOf: 'https://example.com/partial-followers',
next: 'https://example.com/partial-followers/2',
items: [mallory].map { |account| ActivityPub::TagManager.instance.uri_for(account) },
}))
stub_request(:get, 'https://example.com/partial-followers/2')
.to_return(status: 404)
end
it 'confirms pending follow request but does not remove extra followers' do
previous_follower_ids = actor.followers.pluck(:id)
subject.call(actor, collection_uri)
expect(previous_follower_ids - actor.followers.reload.pluck(:id))
.to be_empty
expect(mallory)
.to be_following(actor)
end
end
context 'when the endpoint is a paginated Collection of actor URIs with more pages than we allow' do
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Collection',
id: collection_uri,
first: {
type: 'CollectionPage',
partOf: collection_uri,
items: items,
next: "#{collection_uri}/page2",
},
}.with_indifferent_access
end
before do
stub_const('ActivityPub::SynchronizeFollowersService::MAX_COLLECTION_PAGES', 1)
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end
it 'confirms pending follow request but does not remove extra followers' do
previous_follower_ids = actor.followers.pluck(:id)
subject.call(actor, collection_uri)
expect(previous_follower_ids - actor.followers.reload.pluck(:id))
.to be_empty
expect(mallory)
.to be_following(actor)
end
end
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
RSpec.describe SuspendAccountService, :inline_jobs do
RSpec.describe SuspendAccountService do
shared_examples 'common behavior' do
subject { described_class.new.call(account) }
@ -11,6 +11,7 @@ RSpec.describe SuspendAccountService, :inline_jobs do
before do
allow(FeedManager.instance).to receive_messages(unmerge_from_home: nil, unmerge_from_list: nil)
allow(Rails.configuration.x).to receive(:cache_buster_enabled).and_return(true)
local_follower.follow!(account)
list.accounts << account
@ -23,6 +24,7 @@ RSpec.describe SuspendAccountService, :inline_jobs do
it 'unmerges from feeds of local followers and changes file mode and preserves suspended flag' do
expect { subject }
.to change_file_mode
.and enqueue_sidekiq_job(CacheBusterWorker).with(account.media_attachments.first.file.url(:original))
.and not_change_suspended_flag
expect(FeedManager.instance).to have_received(:unmerge_from_home).with(account, local_follower)
expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list)
@ -38,17 +40,12 @@ RSpec.describe SuspendAccountService, :inline_jobs do
end
describe 'suspending a local account' do
def match_update_actor_request(req, account)
json = JSON.parse(req.body)
def match_update_actor_request(json, account)
json = JSON.parse(json)
actor_id = ActivityPub::TagManager.instance.uri_for(account)
json['type'] == 'Update' && json['actor'] == actor_id && json['object']['id'] == actor_id && json['object']['suspended']
end
before do
stub_request(:post, 'https://alice.com/inbox').to_return(status: 201)
stub_request(:post, 'https://bob.com/inbox').to_return(status: 201)
end
include_examples 'common behavior' do
let!(:account) { Fabricate(:account) }
let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') }
@ -61,22 +58,20 @@ RSpec.describe SuspendAccountService, :inline_jobs do
it 'sends an Update actor activity to followers and reporters' do
subject
expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once
expect(ActivityPub::DeliveryWorker)
.to have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_follower.inbox_url).once
.and have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_reporter.inbox_url).once
end
end
end
describe 'suspending a remote account' do
def match_reject_follow_request(req, account, followee)
json = JSON.parse(req.body)
def match_reject_follow_request(json, account, followee)
json = JSON.parse(json)
json['type'] == 'Reject' && json['actor'] == ActivityPub::TagManager.instance.uri_for(followee) && json['object']['actor'] == account.uri
end
before do
stub_request(:post, 'https://bob.com/inbox').to_return(status: 201)
end
include_examples 'common behavior' do
let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
let!(:local_followee) { Fabricate(:account) }
@ -88,7 +83,8 @@ RSpec.describe SuspendAccountService, :inline_jobs do
it 'sends a Reject Follow activity', :aggregate_failures do
subject
expect(a_request(:post, account.inbox_url).with { |req| match_reject_follow_request(req, account, local_followee) }).to have_been_made.once
expect(ActivityPub::DeliveryWorker)
.to have_enqueued_sidekiq_job(satisfying { |json| match_reject_follow_request(json, account, local_followee) }, local_followee.id, account.inbox_url).once
end
end
end

View file

@ -18,4 +18,24 @@ module SignedRequestHelpers
super(path, headers: headers, **args)
end
def post(path, headers: nil, sign_with: nil, **args)
return super(path, headers: headers, **args) if sign_with.nil?
headers ||= {}
headers['Date'] = Time.now.utc.httpdate
headers['Host'] = Rails.configuration.x.local_domain
headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(args[:params].to_s)}"
signed_headers = headers.merge('(request-target)' => "post #{path}").slice('(request-target)', 'Host', 'Date', 'Digest')
key_id = ActivityPub::TagManager.instance.key_uri_for(sign_with)
keypair = sign_with.keypair
signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
headers['Signature'] = "keyId=\"#{key_id}\",algorithm=\"rsa-sha256\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
super(path, headers: headers, **args)
end
end

View file

@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Account notes', :inline_jobs, :js, :streaming do
include ProfileStories
let(:email) { 'test@example.com' }
let(:password) { 'password' }
let(:confirmed_at) { Time.zone.now }
let(:finished_onboarding) { true }
let!(:other_account) { Fabricate(:account) }
before { as_a_logged_in_user }
it 'can be written and viewed' do
visit_profile(other_account)
note_text = 'This is a personal note'
fill_in 'Click to add note', with: note_text
# This is a bit awkward since there is no button to save the change
# The easiest way is to send ctrl+enter ourselves
find_field(class: 'account__header__account-note__content').send_keys [:control, :enter]
within('.account__header__account-note .inline-alert') do
expect(page)
.to have_content('SAVED')
end
expect(page)
.to have_css('.account__header__account-note__content', text: note_text)
# Navigate back and forth and ensure the comment is still here
visit root_url
visit_profile(other_account)
expect(AccountNote.find_by(account: bob.account, target_account: other_account).comment)
.to eq note_text
expect(page)
.to have_css('.account__header__account-note__content', text: note_text)
end
def visit_profile(account)
visit short_account_path(account)
expect(page)
.to have_css('div.app-holder')
.and have_css('form.compose-form')
end
end

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Admin Account Actions' do
let(:user) { Fabricate(:admin_user) }
before { sign_in user }
describe 'Creating a new account action on an account' do
let(:account) { Fabricate(:account) }
it 'creates the action and redirects to the account page' do
visit new_admin_account_action_path(account_id: account.id)
expect(page)
.to have_title(I18n.t('admin.account_actions.title', acct: account.pretty_acct))
choose(option: 'silence')
expect { submit_form }
.to change { account.strikes.count }.by(1)
expect(page)
.to have_title(account.pretty_acct)
end
def submit_form
click_on I18n.t('admin.account_actions.action')
end
end
end

View file

@ -2,29 +2,33 @@
require 'rails_helper'
RSpec.describe Admin::ActionLogsController do
render_views
RSpec.describe 'Admin Action Logs' do
# Action logs typically cause issues when their targets are not in the database
let!(:account) { Fabricate(:account) }
before do
orphaned_log_types.map do |type|
Fabricate(:action_log, account: account, action: 'destroy', target_type: type, target_id: 1312)
end
populate_action_logs
sign_in Fabricate(:admin_user)
end
describe 'GET #index' do
it 'returns 200' do
sign_in Fabricate(:admin_user)
get :index, params: { page: 1 }
describe 'Viewing action logs' do
it 'shows page with action logs listed' do
visit admin_action_logs_path
expect(response).to have_http_status(200)
expect(page)
.to have_title(I18n.t('admin.action_logs.title'))
.and have_css('.log-entry')
end
end
private
def populate_action_logs
orphaned_log_types.map do |type|
Fabricate(:action_log, account: account, action: 'destroy', target_type: type, target_id: 1312)
end
end
def orphaned_log_types
%w(
Account

View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Admin Change Emails' do
let(:admin) { Fabricate(:admin_user) }
before { sign_in admin }
describe 'Changing the email address for a user', :inline_jobs do
let(:user) { Fabricate :user }
it 'updates user details and sends email' do
visit admin_account_change_email_path(user.account.id)
expect(page)
.to have_title(I18n.t('admin.accounts.change_email.title', username: user.account.username))
fill_in 'user_unconfirmed_email', with: 'test@host.example'
emails = capture_emails { process_change_email }
expect(emails.first)
.to be_present
.and(deliver_to('test@host.example'))
.and(have_subject(/Confirm email/))
expect(page)
.to have_title(user.account.pretty_acct)
end
def process_change_email
expect { click_on I18n.t('admin.accounts.change_email.submit') }
.to not_change { user.reload.email }
.and(change { user.reload.unconfirmed_email }.to('test@host.example'))
.and(change { user.reload.confirmation_token }.from(nil).to(be_present))
end
end
end

View file

@ -2,10 +2,8 @@
require 'rails_helper'
RSpec.describe Admin::DashboardController do
render_views
describe 'GET #index' do
RSpec.describe 'Admin Dashboard' do
describe 'Viewing the dashboard page' do
let(:user) { Fabricate(:owner_user) }
before do
@ -14,14 +12,12 @@ RSpec.describe Admin::DashboardController do
sign_in(user)
end
it 'returns http success and body with system check messages' do
get :index
it 'returns page with system check messages' do
visit admin_dashboard_path
expect(response)
.to have_http_status(200)
.and have_attributes(
body: include(I18n.t('admin.system_checks.software_version_patch_check.message_html'))
)
expect(page)
.to have_title(I18n.t('admin.dashboard.title'))
.and have_content(I18n.t('admin.system_checks.software_version_patch_check.message_html'))
end
private

View file

@ -0,0 +1,58 @@
# frozen_string_literal: true
require 'rails_helper'
require 'webauthn/fake_client'
RSpec.describe 'Admin Users TwoFactorAuthentications' do
let(:user) { Fabricate(:user) }
before { sign_in Fabricate(:admin_user) }
describe 'Disabling 2FA for users' do
before { stub_webauthn_credential }
let(:fake_client) { WebAuthn::FakeClient.new('http://test.host') }
context 'when user has OTP enabled' do
before { user.update(otp_required_for_login: true) }
it 'disables OTP and redirects to admin account page' do
visit admin_account_path(user.account.id)
expect { disable_two_factor }
.to change { user.reload.otp_enabled? }.to(false)
expect(page)
.to have_title(user.account.pretty_acct)
end
end
context 'when user has OTP and WebAuthn enabled' do
before { user.update(otp_required_for_login: true, webauthn_id: WebAuthn.generate_user_id) }
it 'disables OTP and webauthn and redirects to admin account page' do
visit admin_account_path(user.account.id)
expect { disable_two_factor }
.to change { user.reload.otp_enabled? }.to(false)
.and(change { user.reload.webauthn_enabled? }.to(false))
expect(page)
.to have_title(user.account.pretty_acct)
end
end
def disable_two_factor
click_on I18n.t('admin.accounts.disable_two_factor_authentication')
end
def stub_webauthn_credential
public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
Fabricate(
:webauthn_credential,
external_id: public_key_credential.id,
nickname: 'Security Key',
public_key: public_key_credential.public_key,
user_id: user.id
)
end
end
end

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Disputes Strikes' do
before { sign_in(current_user) }
describe 'viewing strike disputes' do
let(:current_user) { Fabricate(:user) }
let!(:strike) { Fabricate(:account_warning, target_account: current_user.account) }
it 'shows a list of strikes and details for each' do
visit disputes_strikes_path
expect(page)
.to have_title(I18n.t('settings.strikes'))
find('.strike-entry').click
expect(page)
.to have_title(strike_page_title)
.and have_content(strike.text)
end
def strike_page_title
I18n.t('disputes.strikes.title', action: I18n.t(strike.action, scope: 'disputes.strikes.title_actions'), date: I18n.l(strike.created_at.to_date))
end
end
end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Filters Statuses' do
describe 'Viewing statuses under a filter' do
let(:filter) { Fabricate(:custom_filter, title: 'good filter') }
context 'with the filter user signed in' do
before { sign_in(filter.account.user) }
it 'returns a page with the status filters' do
visit filter_statuses_path(filter)
expect(page)
.to have_private_cache_control
.and have_title(/good filter/)
end
end
end
end

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Settings Deletes' do
describe 'Deleting user from settings area' do
let(:user) { Fabricate(:user) }
before { sign_in(user) }
it 'requires password and deletes user record', :inline_jobs do
visit settings_delete_path
expect(page)
.to have_title(I18n.t('settings.delete'))
.and have_private_cache_control
# Wrong confirmation value
fill_in 'form_delete_confirmation_password', with: 'wrongvalue'
click_on I18n.t('deletes.proceed')
expect(page)
.to have_content(I18n.t('deletes.challenge_not_passed'))
# Correct confirmation value
fill_in 'form_delete_confirmation_password', with: user.password
click_on I18n.t('deletes.proceed')
expect(page)
.to have_content(I18n.t('deletes.success_msg'))
expect(page)
.to have_title(I18n.t('auth.login'))
expect(User.find_by(id: user.id))
.to be_nil
expect(user.account.reload)
.to be_suspended
expect(CanonicalEmailBlock.block?(user.email))
.to be(false)
end
end
end

View file

@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe DateOfBirthValidator do
let(:record_class) do
Class.new do
include ActiveModel::Validations
attr_accessor :date_of_birth
validates :date_of_birth, date_of_birth: true
end
end
let(:record) { record_class.new }
before do
Setting.min_age = 16
end
describe '#validate_each' do
context 'with an invalid date' do
it 'adds errors' do
record.date_of_birth = '76.830.10'
expect(record).to_not be_valid
expect(record.errors.first.attribute).to eq(:date_of_birth)
expect(record.errors.first.type).to eq(:invalid)
end
end
context 'with a date below age limit' do
it 'adds errors' do
record.date_of_birth = 13.years.ago.strftime('%d.%m.%Y')
expect(record).to_not be_valid
expect(record.errors.first.attribute).to eq(:date_of_birth)
expect(record.errors.first.type).to eq(:below_limit)
end
end
context 'with a date above age limit' do
it 'does not add errors' do
record.date_of_birth = 16.years.ago.strftime('%d.%m.%Y')
expect(record).to be_valid
end
end
end
end