Merge remote-tracking branch 'parent/main' into upstream-20240813
This commit is contained in:
commit
e7ccc0539f
358 changed files with 4653 additions and 4261 deletions
|
@ -227,7 +227,7 @@ describe ApplicationHelper do
|
|||
|
||||
it 'returns an unlock icon for a unlisted visible status' do
|
||||
result = helper.visibility_icon Status.new(visibility: 'unlisted')
|
||||
expect(result).to match(/unlock/)
|
||||
expect(result).to match(/lock_open/)
|
||||
end
|
||||
|
||||
it 'returns a lock icon for a private visible status' do
|
||||
|
@ -237,7 +237,7 @@ describe ApplicationHelper do
|
|||
|
||||
it 'returns an at icon for a direct visible status' do
|
||||
result = helper.visibility_icon Status.new(visibility: 'direct')
|
||||
expect(result).to match(/at/)
|
||||
expect(result).to match(/alternate_email/)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ describe StatusesHelper do
|
|||
it 'returns the correct fa icon' do
|
||||
result = helper.fa_visibility_icon(status)
|
||||
|
||||
expect(result).to match('fa-globe')
|
||||
expect(result).to match('material-globe')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -46,7 +46,7 @@ describe StatusesHelper do
|
|||
it 'returns the correct fa icon' do
|
||||
result = helper.fa_visibility_icon(status)
|
||||
|
||||
expect(result).to match('fa-unlock')
|
||||
expect(result).to match('material-lock_open')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -56,7 +56,7 @@ describe StatusesHelper do
|
|||
it 'returns the correct fa icon' do
|
||||
result = helper.fa_visibility_icon(status)
|
||||
|
||||
expect(result).to match('fa-lock')
|
||||
expect(result).to match('material-lock')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -66,7 +66,7 @@ describe StatusesHelper do
|
|||
it 'returns the correct fa icon' do
|
||||
result = helper.fa_visibility_icon(status)
|
||||
|
||||
expect(result).to match('fa-at')
|
||||
expect(result).to match('material-alternate_email')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
93
spec/lib/paperclip/response_with_limit_adapter_spec.rb
Normal file
93
spec/lib/paperclip/response_with_limit_adapter_spec.rb
Normal file
|
@ -0,0 +1,93 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Paperclip::ResponseWithLimitAdapter do
|
||||
subject { described_class.new(response_with_limit) }
|
||||
|
||||
before { stub_request(:get, url).to_return(headers: headers, body: body) }
|
||||
|
||||
let(:response_with_limit) { ResponseWithLimit.new(response, 50.kilobytes) }
|
||||
let(:response) { Request.new(:get, url).perform(&:itself) }
|
||||
let(:url) { 'https://example.com/dir/foo.png' }
|
||||
let(:headers) { nil }
|
||||
let(:body) { attachment_fixture('600x400.jpeg').binmode.read }
|
||||
|
||||
it 'writes temporary file' do
|
||||
expect(subject.tempfile.read).to eq body
|
||||
expect(subject.size).to eq body.bytesize
|
||||
end
|
||||
|
||||
context 'with Content-Disposition header' do
|
||||
let(:headers) { { 'Content-Disposition' => 'attachment; filename="bar.png"' } }
|
||||
|
||||
it 'uses filename from header' do
|
||||
expect(subject.original_filename).to eq 'bar.png'
|
||||
end
|
||||
|
||||
it 'detects MIME type from content' do
|
||||
expect(subject.content_type).to eq 'image/jpeg'
|
||||
end
|
||||
end
|
||||
|
||||
context 'without Content-Disposition header' do
|
||||
it 'uses filename from path' do
|
||||
expect(subject.original_filename).to eq 'foo.png'
|
||||
end
|
||||
|
||||
it 'detects MIME type from content' do
|
||||
expect(subject.content_type).to eq 'image/jpeg'
|
||||
end
|
||||
end
|
||||
|
||||
context 'without filename in path' do
|
||||
let(:url) { 'https://example.com/' }
|
||||
|
||||
it 'falls back to "data"' do
|
||||
expect(subject.original_filename).to eq 'data'
|
||||
end
|
||||
|
||||
it 'detects MIME type from content' do
|
||||
expect(subject.content_type).to eq 'image/jpeg'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with very long filename' do
|
||||
let(:url) { 'https://example.com/abcdefghijklmnopqrstuvwxyz.0123456789' }
|
||||
|
||||
it 'truncates the filename' do
|
||||
expect(subject.original_filename).to eq 'abcdefghijklmnopqrst.0123'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response size exceeds limit' do
|
||||
context 'with Content-Length header' do
|
||||
let(:headers) { { 'Content-Length' => 5.megabytes } }
|
||||
|
||||
it 'raises without reading the body' do
|
||||
allow(response).to receive(:body).and_call_original
|
||||
|
||||
expect { subject }.to raise_error(Mastodon::LengthValidationError, 'Content-Length 5242880 exceeds limit of 51200')
|
||||
|
||||
expect(response).to_not have_received(:body)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without Content-Length header' do
|
||||
let(:body) { SecureRandom.random_bytes(1.megabyte) }
|
||||
|
||||
it 'raises while reading the body' do
|
||||
expect { subject }.to raise_error(Mastodon::LengthValidationError, 'Body size exceeds limit of 51200')
|
||||
expect(response.content_length).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response times out' do
|
||||
it 'raises' do
|
||||
allow(response.body.connection).to receive(:readpartial).and_raise(HTTP::TimeoutError)
|
||||
|
||||
expect { subject }.to raise_error(HTTP::TimeoutError)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -100,7 +100,7 @@ describe Request do
|
|||
describe "response's body_with_limit method" do
|
||||
it 'rejects body more than 1 megabyte by default' do
|
||||
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes))
|
||||
expect { subject.perform(&:body_with_limit) }.to raise_error Mastodon::LengthValidationError
|
||||
expect { subject.perform(&:body_with_limit) }.to raise_error(Mastodon::LengthValidationError, 'Body size exceeds limit of 1048576')
|
||||
end
|
||||
|
||||
it 'accepts body less than 1 megabyte by default' do
|
||||
|
@ -110,17 +110,17 @@ describe Request do
|
|||
|
||||
it 'rejects body by given size' do
|
||||
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.kilobytes))
|
||||
expect { subject.perform { |response| response.body_with_limit(1.kilobyte) } }.to raise_error Mastodon::LengthValidationError
|
||||
expect { subject.perform { |response| response.body_with_limit(1.kilobyte) } }.to raise_error(Mastodon::LengthValidationError, 'Body size exceeds limit of 1024')
|
||||
end
|
||||
|
||||
it 'rejects too large chunked body' do
|
||||
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Transfer-Encoding' => 'chunked' })
|
||||
expect { subject.perform(&:body_with_limit) }.to raise_error Mastodon::LengthValidationError
|
||||
expect { subject.perform(&:body_with_limit) }.to raise_error(Mastodon::LengthValidationError, 'Body size exceeds limit of 1048576')
|
||||
end
|
||||
|
||||
it 'rejects too large monolithic body' do
|
||||
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Content-Length' => 2.megabytes })
|
||||
expect { subject.perform(&:body_with_limit) }.to raise_error Mastodon::LengthValidationError
|
||||
expect { subject.perform(&:body_with_limit) }.to raise_error(Mastodon::LengthValidationError, 'Content-Length 2097152 exceeds limit of 1048576')
|
||||
end
|
||||
|
||||
it 'truncates large monolithic body' do
|
||||
|
|
|
@ -292,6 +292,25 @@ RSpec.describe MediaAttachment, :attachment_processing do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'cache deletion hooks' do
|
||||
let(:media) { Fabricate(:media_attachment) }
|
||||
|
||||
before do
|
||||
allow(Rails.configuration.x).to receive(:cache_buster_enabled).and_return(true)
|
||||
end
|
||||
|
||||
it 'queues CacheBusterWorker jobs' do
|
||||
original_path = media.file.path(:original)
|
||||
small_path = media.file.path(:small)
|
||||
thumbnail_path = media.thumbnail.path(:original)
|
||||
|
||||
expect { media.destroy }
|
||||
.to enqueue_sidekiq_job(CacheBusterWorker).with(original_path)
|
||||
.and enqueue_sidekiq_job(CacheBusterWorker).with(small_path)
|
||||
.and enqueue_sidekiq_job(CacheBusterWorker).with(thumbnail_path)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def media_metadata
|
||||
|
|
|
@ -9,7 +9,7 @@ RSpec.describe NotificationPolicy do
|
|||
let(:sender) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
Fabricate.times(2, :notification, account: subject.account, activity: Fabricate(:status, account: sender), filtered: true)
|
||||
Fabricate.times(2, :notification, account: subject.account, activity: Fabricate(:status, account: sender), filtered: true, type: :mention)
|
||||
Fabricate(:notification_request, account: subject.account, from_account: sender)
|
||||
subject.summarize!
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ RSpec.describe NotificationRequest do
|
|||
|
||||
context 'when there are remaining notifications' do
|
||||
before do
|
||||
Fabricate(:notification, account: subject.account, activity: Fabricate(:status, account: subject.from_account), filtered: true)
|
||||
Fabricate(:notification, account: subject.account, activity: Fabricate(:status, account: subject.from_account), filtered: true, type: :mention)
|
||||
subject.reconsider_existence!
|
||||
end
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ RSpec.describe 'Policies' do
|
|||
|
||||
it 'changes notification policy and returns an updated json object', :aggregate_failures do
|
||||
expect { subject }
|
||||
.to change { NotificationPolicy.find_or_initialize_by(account: user.account).filter_not_following }.from(false).to(true)
|
||||
.to change { NotificationPolicy.find_or_initialize_by(account: user.account).for_not_following.to_sym }.from(:accept).to(:filter)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json).to include(
|
||||
|
|
72
spec/requests/api/v2/notifications/policies_spec.rb
Normal file
72
spec/requests/api/v2/notifications/policies_spec.rb
Normal file
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Policies' do
|
||||
let(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) }
|
||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||
let(:scopes) { 'read:notifications write:notifications' }
|
||||
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||
|
||||
describe 'GET /api/v2/notifications/policy', :inline_jobs do
|
||||
subject do
|
||||
get '/api/v2/notifications/policy', headers: headers, params: params
|
||||
end
|
||||
|
||||
let(:params) { {} }
|
||||
|
||||
before do
|
||||
Fabricate(:notification_request, account: user.account)
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for wrong scope', 'write write:notifications'
|
||||
|
||||
context 'with no options' do
|
||||
it 'returns json with expected attributes', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json).to include(
|
||||
for_not_following: 'accept',
|
||||
for_not_followers: 'accept',
|
||||
for_new_accounts: 'accept',
|
||||
for_private_mentions: 'filter',
|
||||
for_limited_accounts: 'filter',
|
||||
summary: a_hash_including(
|
||||
pending_requests_count: 1,
|
||||
pending_notifications_count: 0
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v2/notifications/policy' do
|
||||
subject do
|
||||
put '/api/v2/notifications/policy', headers: headers, params: params
|
||||
end
|
||||
|
||||
let(:params) { { for_not_following: 'filter', for_limited_accounts: 'drop' } }
|
||||
|
||||
it_behaves_like 'forbidden for wrong scope', 'read read:notifications'
|
||||
|
||||
it 'changes notification policy and returns an updated json object', :aggregate_failures do
|
||||
expect { subject }
|
||||
.to change { NotificationPolicy.find_or_initialize_by(account: user.account).for_not_following.to_sym }.from(:accept).to(:filter)
|
||||
.and change { NotificationPolicy.find_or_initialize_by(account: user.account).for_limited_accounts.to_sym }.from(:filter).to(:drop)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json).to include(
|
||||
for_not_following: 'filter',
|
||||
for_not_followers: 'accept',
|
||||
for_new_accounts: 'accept',
|
||||
for_private_mentions: 'filter',
|
||||
for_limited_accounts: 'drop',
|
||||
summary: a_hash_including(
|
||||
pending_requests_count: 0,
|
||||
pending_notifications_count: 0
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -160,6 +160,36 @@ RSpec.describe 'Notifications' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when requesting stripped-down accounts' do
|
||||
let(:params) { { expand_accounts: 'partial_avatars' } }
|
||||
|
||||
let(:recent_account) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
FavouriteService.new.call(recent_account, user.account.statuses.first)
|
||||
end
|
||||
|
||||
it 'returns an account in "partial_accounts", with the expected keys', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(body_as_json[:partial_accounts].size).to be > 0
|
||||
expect(body_as_json[:partial_accounts][0].keys).to contain_exactly(:acct, :avatar, :avatar_static, :bot, :id, :locked, :url)
|
||||
expect(body_as_json[:partial_accounts].pluck(:id)).to_not include(recent_account.id.to_s)
|
||||
expect(body_as_json[:accounts].pluck(:id)).to include(recent_account.id.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing an invalid value for "expand_accounts"' do
|
||||
let(:params) { { expand_accounts: 'unknown_foobar' } }
|
||||
|
||||
it 'returns http bad request' do
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(400)
|
||||
end
|
||||
end
|
||||
|
||||
def body_json_types
|
||||
body_as_json[:notification_groups].pluck(:type)
|
||||
end
|
||||
|
|
19
spec/services/dismiss_notification_request_service_spec.rb
Normal file
19
spec/services/dismiss_notification_request_service_spec.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe DismissNotificationRequestService do
|
||||
describe '#call' do
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:receiver) { Fabricate(:account) }
|
||||
let(:request) { Fabricate(:notification_request, account: receiver, from_account: sender) }
|
||||
|
||||
it 'destroys the request and queues a worker', :aggregate_failures do
|
||||
expect { described_class.new.call(request) }
|
||||
.to change(request, :destroyed?).to(true)
|
||||
|
||||
expect(FilteredNotificationCleanupWorker)
|
||||
.to have_enqueued_sidekiq_job(receiver.id, sender.id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -129,6 +129,40 @@ RSpec.describe NotifyService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the blocked sender has a role' do
|
||||
let(:sender) { Fabricate(:user, role: sender_role).account }
|
||||
let(:activity) { Fabricate(:mention, status: Fabricate(:status, account: sender)) }
|
||||
let(:type) { :mention }
|
||||
|
||||
before do
|
||||
recipient.block!(sender)
|
||||
end
|
||||
|
||||
context 'when the role is a visible moderator' do
|
||||
let(:sender_role) { Fabricate(:user_role, highlighted: true, permissions: UserRole::FLAGS[:manage_users]) }
|
||||
|
||||
it 'does notify' do
|
||||
expect { subject }.to change(Notification, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the role is a non-visible moderator' do
|
||||
let(:sender_role) { Fabricate(:user_role, highlighted: false, permissions: UserRole::FLAGS[:manage_users]) }
|
||||
|
||||
it 'does not notify' do
|
||||
expect { subject }.to_not change(Notification, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the role is a visible non-moderator' do
|
||||
let(:sender_role) { Fabricate(:user_role, highlighted: true) }
|
||||
|
||||
it 'does not notify' do
|
||||
expect { subject }.to_not change(Notification, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with filtered notifications' do
|
||||
let(:unknown) { Fabricate(:account, username: 'unknown') }
|
||||
let(:status) { Fabricate(:status, account: unknown) }
|
||||
|
@ -162,20 +196,58 @@ RSpec.describe NotifyService do
|
|||
end
|
||||
end
|
||||
|
||||
describe NotifyService::DismissCondition do
|
||||
describe NotifyService::DropCondition do
|
||||
subject { described_class.new(notification) }
|
||||
|
||||
let(:activity) { Fabricate(:mention, status: Fabricate(:status)) }
|
||||
let(:notification) { Fabricate(:notification, type: :mention, activity: activity, from_account: activity.status.account, account: activity.account) }
|
||||
|
||||
describe '#dismiss?' do
|
||||
context 'when sender is silenced' do
|
||||
describe '#drop' do
|
||||
context 'when sender is silenced and recipient has a default policy' do
|
||||
before do
|
||||
notification.from_account.silence!
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(subject.dismiss?).to be false
|
||||
expect(subject.drop?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sender is silenced and recipient has a policy to ignore silenced accounts' do
|
||||
before do
|
||||
notification.from_account.silence!
|
||||
notification.account.create_notification_policy!(for_limited_accounts: :drop)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(subject.drop?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sender is new and recipient has a default policy' do
|
||||
it 'returns false' do
|
||||
expect(subject.drop?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sender is new and recipient has a policy to ignore silenced accounts' do
|
||||
before do
|
||||
notification.account.create_notification_policy!(for_new_accounts: :drop)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(subject.drop?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sender is new and followed and recipient has a policy to ignore silenced accounts' do
|
||||
before do
|
||||
notification.account.create_notification_policy!(for_new_accounts: :drop)
|
||||
notification.account.follow!(notification.from_account)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(subject.drop?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -185,7 +257,7 @@ RSpec.describe NotifyService do
|
|||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(subject.dismiss?).to be true
|
||||
expect(subject.drop?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -216,6 +288,16 @@ RSpec.describe NotifyService do
|
|||
expect(subject.filter?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when recipient is allowing limited accounts' do
|
||||
before do
|
||||
notification.account.create_notification_policy!(for_limited_accounts: :accept)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(subject.filter?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when recipient is filtering not-followed senders' do
|
||||
|
|
24
spec/workers/filtered_notification_cleanup_worker_spec.rb
Normal file
24
spec/workers/filtered_notification_cleanup_worker_spec.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe FilteredNotificationCleanupWorker do
|
||||
describe '#perform' do
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
let(:bystander) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
Fabricate(:notification, account: recipient, activity: Fabricate(:favourite, account: sender), filtered: true)
|
||||
Fabricate(:notification, account: recipient, activity: Fabricate(:favourite, account: bystander), filtered: true)
|
||||
Fabricate(:notification, account: recipient, activity: Fabricate(:follow, account: sender), filtered: true)
|
||||
Fabricate(:notification, account: recipient, activity: Fabricate(:favourite, account: bystander), filtered: true)
|
||||
end
|
||||
|
||||
it 'deletes all filtered notifications to the account' do
|
||||
expect { described_class.new.perform(recipient.id, sender.id) }
|
||||
.to change { recipient.notifications.where(from_account: sender).count }.from(2).to(0)
|
||||
.and(not_change { recipient.notifications.where(from_account: bystander).count })
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue