Merge remote-tracking branch 'parent/main' into upstream-20250328
This commit is contained in:
commit
12ed20b6d5
257 changed files with 3505 additions and 2010 deletions
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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' } }
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
32
spec/lib/mastodon/middleware/prometheus_queue_time_spec.rb
Normal file
32
spec/lib/mastodon/middleware/prometheus_queue_time_spec.rb
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
148
spec/requests/activitypub/inboxes_spec.rb
Normal file
148
spec/requests/activitypub/inboxes_spec.rb
Normal 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
|
|
@ -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(
|
|
@ -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
|
|
@ -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' }
|
||||
|
|
|
@ -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
|
||||
|
|
48
spec/requests/api/v1/trends_spec.rb
Normal file
48
spec/requests/api/v1/trends_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
22
spec/requests/disputes/strikes_spec.rb
Normal file
22
spec/requests/disputes/strikes_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
53
spec/system/account_notes_spec.rb
Normal file
53
spec/system/account_notes_spec.rb
Normal 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
|
29
spec/system/admin/account_actions_spec.rb
Normal file
29
spec/system/admin/account_actions_spec.rb
Normal 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
|
|
@ -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
|
35
spec/system/admin/change_emails_spec.rb
Normal file
35
spec/system/admin/change_emails_spec.rb
Normal 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
|
|
@ -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
|
58
spec/system/admin/users/two_factor_authentications_spec.rb
Normal file
58
spec/system/admin/users/two_factor_authentications_spec.rb
Normal 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
|
27
spec/system/disputes/strikes_spec.rb
Normal file
27
spec/system/disputes/strikes_spec.rb
Normal 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
|
21
spec/system/filters/statuses_spec.rb
Normal file
21
spec/system/filters/statuses_spec.rb
Normal 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
|
38
spec/system/settings/deletes_spec.rb
Normal file
38
spec/system/settings/deletes_spec.rb
Normal 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
|
51
spec/validators/date_of_birth_validator_spec.rb
Normal file
51
spec/validators/date_of_birth_validator_spec.rb
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue