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

This commit is contained in:
KMY 2025-04-14 13:23:00 +09:00
commit dba5f3b93f
208 changed files with 3210 additions and 2896 deletions

View file

@ -1,7 +0,0 @@
# frozen_string_literal: true
Fabricator(:import) do
account
type :following
data { attachment_fixture('imports.txt') }
end

View file

@ -1,10 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Import do
describe 'Validations' do
it { is_expected.to validate_presence_of(:type) }
it { is_expected.to validate_presence_of(:data) }
end
end

View file

@ -1,242 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ImportService, :inline_jobs do
include RoutingHelper
let!(:account) { Fabricate(:account, locked: false) }
let!(:bob) { Fabricate(:account, username: 'bob', locked: false) }
let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false, protocol: :activitypub, inbox_url: 'https://example.com/inbox') }
before do
stub_request(:post, 'https://example.com/inbox').to_return(status: 200)
end
context 'when importing old-style list of muted users' do
subject { described_class.new }
let(:csv) { attachment_fixture('mute-imports.txt') }
describe 'when no accounts are muted' do
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
it 'mutes the listed accounts, including notifications' do
subject.call(import)
expect(account.muting.count).to eq 2
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
end
end
describe 'when some accounts are muted and overwrite is not set' do
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
it 'mutes the listed accounts, including notifications' do
account.mute!(bob, notifications: false)
subject.call(import)
expect(account.muting.count).to eq 2
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
end
end
describe 'when some accounts are muted and overwrite is set' do
let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) }
it 'mutes the listed accounts, including notifications' do
account.mute!(bob, notifications: false)
subject.call(import)
expect(account.muting.count).to eq 2
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
end
end
end
context 'when importing new-style list of muted users' do
subject { described_class.new }
let(:csv) { attachment_fixture('new-mute-imports.txt') }
describe 'when no accounts are muted' do
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
it 'mutes the listed accounts, respecting notifications' do
subject.call(import)
expect(account.muting.count).to eq 2
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
end
end
describe 'when some accounts are muted and overwrite is not set' do
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
it 'mutes the listed accounts, respecting notifications' do
account.mute!(bob, notifications: true)
subject.call(import)
expect(account.muting.count).to eq 2
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
end
end
describe 'when some accounts are muted and overwrite is set' do
let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) }
it 'mutes the listed accounts, respecting notifications' do
account.mute!(bob, notifications: true)
subject.call(import)
expect(account.muting.count).to eq 2
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
end
end
end
context 'when importing old-style list of followed users' do
subject { described_class.new }
let(:csv) { attachment_fixture('mute-imports.txt') }
describe 'when no accounts are followed' do
let(:import) { Import.create(account: account, type: 'following', data: csv) }
it 'follows the listed accounts, including boosts' do
subject.call(import)
expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
end
end
describe 'when some accounts are already followed and overwrite is not set' do
let(:import) { Import.create(account: account, type: 'following', data: csv) }
it 'follows the listed accounts, including notifications' do
account.follow!(bob, reblogs: false)
subject.call(import)
expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
end
end
describe 'when some accounts are already followed and overwrite is set' do
let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) }
it 'mutes the listed accounts, including notifications' do
account.follow!(bob, reblogs: false)
subject.call(import)
expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
end
end
end
context 'when importing new-style list of followed users' do
subject { described_class.new }
let(:csv) { attachment_fixture('new-following-imports.txt') }
describe 'when no accounts are followed' do
let(:import) { Import.create(account: account, type: 'following', data: csv) }
it 'follows the listed accounts, respecting boosts' do
subject.call(import)
expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
end
end
describe 'when some accounts are already followed and overwrite is not set' do
let(:import) { Import.create(account: account, type: 'following', data: csv) }
it 'mutes the listed accounts, respecting notifications' do
account.follow!(bob, reblogs: true)
subject.call(import)
expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
end
end
describe 'when some accounts are already followed and overwrite is set' do
let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) }
it 'mutes the listed accounts, respecting notifications' do
account.follow!(bob, reblogs: true)
subject.call(import)
expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
end
end
end
# Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users
#
# https://github.com/mastodon/mastodon/issues/20571
context 'with a utf-8 encoded domains' do
subject { described_class.new }
let!(:nare) { Fabricate(:account, username: 'nare', domain: 'թութ.հայ', locked: false, protocol: :activitypub, inbox_url: 'https://թութ.հայ/inbox') }
let(:csv) { attachment_fixture('utf8-followers.txt') }
let(:import) { Import.create(account: account, type: 'following', data: csv) }
# Make sure to not actually go to the remote server
before do
stub_request(:post, nare.inbox_url).to_return(status: 200)
end
it 'follows the listed account' do
expect(account.follow_requests.count).to eq 0
subject.call(import)
expect(account.follow_requests.count).to eq 1
end
end
context 'when importing bookmarks' do
subject { described_class.new }
let(:csv) { attachment_fixture('bookmark-imports.txt') }
let(:local_account) { Fabricate(:account, username: 'foo', domain: nil) }
let!(:remote_status) { Fabricate(:status, uri: 'https://example.com/statuses/1312') }
let!(:direct_status) { Fabricate(:status, uri: 'https://example.com/statuses/direct', visibility: :direct) }
around do |example|
local_before = Rails.configuration.x.local_domain
web_before = Rails.configuration.x.web_domain
Rails.configuration.x.local_domain = 'local.com'
Rails.configuration.x.web_domain = 'local.com'
example.run
Rails.configuration.x.web_domain = web_before
Rails.configuration.x.local_domain = local_before
end
before do
service = instance_double(ActivityPub::FetchRemoteStatusService)
allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service)
allow(service).to receive(:call).with('https://unknown-remote.com/users/bar/statuses/1') do
Fabricate(:status, uri: 'https://unknown-remote.com/users/bar/statuses/1')
end
end
describe 'when no bookmarks are set' do
let(:import) { Import.create(account: account, type: 'bookmarks', data: csv) }
it 'adds the toots the user has access to to bookmarks' do
local_status = Fabricate(:status, account: local_account, uri: 'https://local.com/users/foo/statuses/42', id: 42, local: true)
subject.call(import)
expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to include(local_status.id)
expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to include(remote_status.id)
expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to_not include(direct_status.id)
expect(account.bookmarks.count).to eq 3
end
end
end
end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
module SystemHelpers
FRONTEND_TRANSLATIONS = JSON.parse Rails.root.join('app', 'javascript', 'mastodon', 'locales', 'en.json').read
def submit_button
I18n.t('generic.save_changes')
end
@ -16,4 +18,8 @@ module SystemHelpers
def css_id(record)
"##{dom_id(record)}"
end
def frontend_translations(key)
FRONTEND_TRANSLATIONS[key]
end
end

View file

@ -18,7 +18,7 @@ RSpec.describe 'Account notes', :inline_jobs, :js, :streaming do
visit_profile(other_account)
note_text = 'This is a personal note'
fill_in 'Click to add note', with: note_text
fill_in frontend_translations('account_note.placeholder'), 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

View file

@ -17,8 +17,9 @@ RSpec.describe 'Log out' do
click_on 'Logout'
end
expect(page).to have_title(I18n.t('auth.login'))
expect(page).to have_current_path('/auth/sign_in')
expect(page)
.to have_title(I18n.t('auth.login'))
.and have_current_path('/auth/sign_in')
end
end
@ -28,6 +29,8 @@ RSpec.describe 'Log out' do
ignore_js_error(/Failed to load resource: the server responded with a status of 422/)
visit root_path
expect(page)
.to have_css('body', class: 'app-body')
within '.navigation-bar' do
click_on 'Menu'
@ -39,8 +42,9 @@ RSpec.describe 'Log out' do
click_on 'Log out'
expect(page).to have_title(I18n.t('auth.login'))
expect(page).to have_current_path('/auth/sign_in')
expect(page)
.to have_title(I18n.t('auth.login'))
.and have_current_path('/auth/sign_in')
end
end
end

View file

@ -20,20 +20,7 @@ RSpec.describe 'NewStatuses', :inline_jobs, :js, :streaming do
status_text = 'This is a new status!'
within('.compose-form') do
fill_in "What's on your mind?", with: status_text
click_on 'Post'
end
expect(page)
.to have_css('.status__content__text', text: status_text)
end
it 'can be posted again' do
visit_homepage
status_text = 'This is a second status!'
within('.compose-form') do
fill_in "What's on your mind?", with: status_text
fill_in frontend_translations('compose_form.placeholder'), with: status_text
click_on 'Post'
end

View file

@ -23,24 +23,14 @@ RSpec.describe 'Share page', :js, :streaming do
fill_in_form
expect(page)
.to have_css('.notification-bar-message', text: translations['compose.published.body'])
.to have_css('.notification-bar-message', text: frontend_translations('compose.published.body'))
end
def fill_in_form
within('.compose-form') do
fill_in translations['compose_form.placeholder'],
fill_in frontend_translations('compose_form.placeholder'),
with: 'This is a new status!'
click_on translations['compose_form.publish']
click_on frontend_translations('compose_form.publish')
end
end
def translations
# TODO: Extract to system spec helper for re-use?
JSON.parse(
Rails
.root
.join('app', 'javascript', 'mastodon', 'locales', 'en.json')
.read
)
end
end

View file

@ -1,23 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ImportWorker do
let(:worker) { described_class.new }
let(:service) { instance_double(ImportService, call: true) }
describe '#perform' do
before do
allow(ImportService).to receive(:new).and_return(service)
end
let(:import) { Fabricate(:import) }
it 'sends the import to the service' do
worker.perform(import.id)
expect(service).to have_received(:call).with(import)
expect { import.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end

View file

@ -5,6 +5,7 @@ require 'rails_helper'
RSpec.describe UnfilterNotificationsWorker do
let(:recipient) { Fabricate(:account) }
let(:sender) { Fabricate(:account) }
let(:worker) { described_class.new }
before do
# Populate multiple kinds of filtered notifications
@ -67,23 +68,22 @@ RSpec.describe UnfilterNotificationsWorker do
end
describe '#perform' do
context 'with single argument (prerelease behavior)' do
subject { described_class.new.perform(notification_request.id) }
let(:notification_request) { Fabricate(:notification_request, from_account: sender, account: recipient) }
context 'with recipient and sender' do
subject { worker.perform(recipient.id, sender.id) }
it_behaves_like 'shared behavior'
it 'destroys the notification request' do
expect { subject }
.to change { NotificationRequest.exists?(notification_request.id) }.to(false)
end
end
context 'with two arguments' do
subject { described_class.new.perform(recipient.id, sender.id) }
context 'with missing records' do
it 'runs without error for missing sender' do
expect { worker.perform(recipient.id, nil) }
.to_not raise_error
end
it_behaves_like 'shared behavior'
it 'runs without error for missing recipient' do
expect { worker.perform(nil, sender.id) }
.to_not raise_error
end
end
end
end