Merge remote-tracking branch 'parent/main' into kb-upstream-20231018

This commit is contained in:
KMY 2023-10-18 11:13:56 +09:00
commit 527ded889d
32 changed files with 556 additions and 586 deletions

View file

@ -106,6 +106,9 @@ group :test do
# Used to split testing into chunks in CI # Used to split testing into chunks in CI
gem 'rspec_chunked', '~> 0.6' gem 'rspec_chunked', '~> 0.6'
# Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab
gem 'rspec-github', '~> 2.4', require: false
# RSpec progress bar formatter # RSpec progress bar formatter
gem 'fuubar', '~> 2.5' gem 'fuubar', '~> 2.5'

View file

@ -513,7 +513,7 @@ GEM
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0) private_address_check (0.5.0)
public_suffix (5.0.3) public_suffix (5.0.3)
puma (6.3.1) puma (6.4.0)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -602,6 +602,8 @@ GEM
rspec-expectations (3.12.3) rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-github (2.4.0)
rspec-core (~> 3.0)
rspec-mocks (3.12.5) rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
@ -636,11 +638,11 @@ GEM
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0) rubocop-ast (1.29.0)
parser (>= 3.2.1.0) parser (>= 3.2.1.0)
rubocop-capybara (2.18.0) rubocop-capybara (2.19.0)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-factory_bot (2.23.1) rubocop-factory_bot (2.23.1)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-performance (1.19.0) rubocop-performance (1.19.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rails (2.20.2) rubocop-rails (2.20.2)
@ -887,6 +889,7 @@ DEPENDENCIES
redis (~> 4.5) redis (~> 4.5)
redis-namespace (~> 1.10) redis-namespace (~> 1.10)
rqrcode (~> 2.2) rqrcode (~> 2.2)
rspec-github (~> 2.4)
rspec-rails (~> 6.0) rspec-rails (~> 6.0)
rspec-retry (>= 0.6.2) rspec-retry (>= 0.6.2)
rspec-sidekiq (~> 4.0) rspec-sidekiq (~> 4.0)

View file

@ -66,7 +66,7 @@ class Search extends PureComponent {
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } }, { label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } }, { label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } }, { label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } }, { label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library', 'public']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } },
{ label: <><mark>order:</mark> <FormattedList type='disjunction' value={['desc', 'asc']} /></>, action: e => { e.preventDefault(); this._insertText('order:'); } }, { label: <><mark>order:</mark> <FormattedList type='disjunction' value={['desc', 'asc']} /></>, action: e => { e.preventDefault(); this._insertText('order:'); } },
]; ];

View file

@ -198,7 +198,9 @@ class SwitchingColumnsArea extends PureComponent {
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null} {singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null} {singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
{/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */}
{!singleColumn && pathName === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null} {!singleColumn && pathName === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null}
{!singleColumn && pathName === '/home' ? <Redirect from='/home' to='/deck/getting-started' exact /> : null}
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />

View file

@ -113,6 +113,7 @@ const initialPath = document.querySelector("head meta[name=initialPath]")?.getAt
/** @type {boolean} */ /** @type {boolean} */
export const hasMultiColumnPath = initialPath === '/' export const hasMultiColumnPath = initialPath === '/'
|| initialPath === '/getting-started' || initialPath === '/getting-started'
|| initialPath === '/home'
|| initialPath.startsWith('/deck'); || initialPath.startsWith('/deck');
/** /**

View file

@ -79,6 +79,8 @@ class SearchQueryTransformer < Parslet::Transform
case @flags['in'] case @flags['in']
when 'library' when 'library'
[StatusesIndex] [StatusesIndex]
when 'public'
[PublicStatusesIndex]
else else
@options[:current_account].user&.setting_use_public_index ? [PublicStatusesIndex, StatusesIndex] : [StatusesIndex] @options[:current_account].user&.setting_use_public_index ? [PublicStatusesIndex, StatusesIndex] : [StatusesIndex]
end end

View file

@ -11,16 +11,31 @@ class UnreservedUsernameValidator < ActiveModel::Validator
private private
def pam_controlled? def reserved_username?
return false unless Devise.pam_authentication && Devise.pam_controlled_service pam_username_reserved? || settings_username_reserved?
Rpam2.account(Devise.pam_controlled_service, @username).present?
end end
def reserved_username? def pam_username_reserved?
return true if pam_controlled? pam_controlled? && pam_reserves_username?
return false unless Setting.reserved_usernames end
def pam_controlled?
Devise.pam_authentication && Devise.pam_controlled_service
end
def pam_reserves_username?
Rpam2.account(Devise.pam_controlled_service, @username)
end
def settings_username_reserved?
settings_has_reserved_usernames? && settings_reserves_username?
end
def settings_has_reserved_usernames?
Setting.reserved_usernames.present?
end
def settings_reserves_username?
Setting.reserved_usernames.include?(@username.downcase) Setting.reserved_usernames.include?(@username.downcase)
end end
end end

View file

@ -33,76 +33,6 @@
], ],
"note": "" "note": ""
}, },
{
"warning_type": "Denial of Service",
"warning_code": 76,
"fingerprint": "7b6abba5699755348e7ee82a4694bfbf574b41c7cce2d0db0f7c11ae3f983c72",
"check_name": "RegexDoS",
"message": "Model attribute used in regular expression",
"file": "lib/mastodon/cli/domains.rb",
"line": 128,
"link": "https://brakemanscanner.org/docs/warning_types/denial_of_service/",
"code": "/\\.?(#{DomainBlock.where(:severity => 1).pluck(:domain).map do\n Regexp.escape(domain)\n end.join(\"|\")})$/",
"render_path": null,
"location": {
"type": "method",
"class": "Mastodon::CLI::Domains",
"method": "crawl"
},
"user_input": "DomainBlock.where(:severity => 1).pluck(:domain)",
"confidence": "Weak",
"cwe_id": [
20,
185
],
"note": ""
},
{
"warning_type": "Mass Assignment",
"warning_code": 105,
"fingerprint": "874be88fedf4c680926845e9a588d3197765a6ccbfdd76466b44cc00151c612e",
"check_name": "PermitAttributes",
"message": "Potentially dangerous key allowed for mass assignment",
"file": "app/controllers/api/v1/admin/reports_controller.rb",
"line": 88,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.permit(:resolved, :account_id, :target_account_id)",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V1::Admin::ReportsController",
"method": "filter_params"
},
"user_input": ":account_id",
"confidence": "High",
"cwe_id": [
915
],
"note": ""
},
{
"warning_type": "Mass Assignment",
"warning_code": 105,
"fingerprint": "ab5035dd1a9f8c3a8d92fb2c37e8fe86fede4f87c91b71aa32e89c9eede602fc",
"check_name": "PermitAttributes",
"message": "Potentially dangerous key allowed for mass assignment",
"file": "app/controllers/api/v1/notifications_controller.rb",
"line": 81,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.permit(:account_id, :types => ([]), :exclude_types => ([]))",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V1::NotificationsController",
"method": "browserable_params"
},
"user_input": ":account_id",
"confidence": "High",
"cwe_id": [
915
],
"note": ""
},
{ {
"warning_type": "Cross-Site Scripting", "warning_type": "Cross-Site Scripting",
"warning_code": 4, "warning_code": 4,

View file

@ -111,7 +111,7 @@ services:
test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"] test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"]
## Uncomment to enable federation with tor instances along with adding the following ENV variables ## Uncomment to enable federation with tor instances along with adding the following ENV variables
## http_proxy=http://privoxy:8118 ## http_hidden_proxy=http://privoxy:8118
## ALLOW_ACCESS_TO_HIDDEN_SERVICE=true ## ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
# tor: # tor:
# image: sirboops/tor # image: sirboops/tor

View file

@ -125,7 +125,7 @@ module Mastodon::CLI
failed = Concurrent::AtomicFixnum.new(0) failed = Concurrent::AtomicFixnum.new(0)
start_at = Time.now.to_f start_at = Time.now.to_f
seed = start ? [start] : Instance.pluck(:domain) seed = start ? [start] : Instance.pluck(:domain)
blocked_domains = /\.?(#{DomainBlock.where(severity: 1).pluck(:domain).map { |domain| Regexp.escape(domain) }.join('|')})$/ blocked_domains = /\.?(#{Regexp.union(domain_block_suspended_domains).source})$/
progress = create_progress_bar progress = create_progress_bar
pool = Concurrent::ThreadPoolExecutor.new(min_threads: 0, max_threads: options[:concurrency], idletime: 10, auto_terminate: true, max_queue: 0) pool = Concurrent::ThreadPoolExecutor.new(min_threads: 0, max_threads: options[:concurrency], idletime: 10, auto_terminate: true, max_queue: 0)
@ -189,6 +189,10 @@ module Mastodon::CLI
private private
def domain_block_suspended_domains
DomainBlock.suspend.pluck(:domain)
end
def stats_to_summary(stats, processed, failed, start_at) def stats_to_summary(stats, processed, failed, start_at)
stats.compact! stats.compact!

View file

@ -7,449 +7,319 @@ RSpec.describe AccountsController do
let(:account) { Fabricate(:account) } let(:account) { Fabricate(:account) }
describe 'GET #show' do shared_examples 'unapproved account check' do
let(:format) { 'html' } before { account.user.update(approved: false) }
let!(:status) { Fabricate(:status, account: account) } it 'returns http not found' do
let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) } get :show, params: { username: account.username, format: format }
let!(:status_self_reply) { Fabricate(:status, account: account, thread: status) }
let!(:status_media) { Fabricate(:status, account: account) }
let!(:status_pinned) { Fabricate(:status, account: account) }
let!(:status_private) { Fabricate(:status, account: account, visibility: :private) }
let!(:status_direct) { Fabricate(:status, account: account, visibility: :direct) }
let!(:status_reblog) { Fabricate(:status, account: account, reblog: Fabricate(:status)) }
expect(response).to have_http_status(404)
end
end
shared_examples 'permanently suspended account check' do
before do before do
status_media.media_attachments << Fabricate(:media_attachment, account: account, type: :image) account.suspend!
account.pinned_statuses << status_pinned account.deletion_request.destroy
account.pinned_statuses << status_private
end end
shared_examples 'preliminary checks' do it 'returns http gone' do
context 'when account is not approved' do get :show, params: { username: account.username, format: format }
before do
account.user.update(approved: false)
end
it 'returns http not found' do expect(response).to have_http_status(410)
get :show, params: { username: account.username, format: format } end
expect(response).to have_http_status(404) end
end
shared_examples 'temporarily suspended account check' do |code: 403|
before { account.suspend! }
it 'returns appropriate http response code' do
get :show, params: { username: account.username, format: format }
expect(response).to have_http_status(code)
end
end
describe 'GET #show' do
context 'with basic account status checks' do
context 'with HTML' do
let(:format) { 'html' }
it_behaves_like 'unapproved account check'
it_behaves_like 'permanently suspended account check'
it_behaves_like 'temporarily suspended account check'
end
context 'with JSON' do
let(:format) { 'json' }
it_behaves_like 'unapproved account check'
it_behaves_like 'permanently suspended account check'
it_behaves_like 'temporarily suspended account check', code: 200
end
context 'with RSS' do
let(:format) { 'rss' }
it_behaves_like 'unapproved account check'
it_behaves_like 'permanently suspended account check'
it_behaves_like 'temporarily suspended account check'
end end
end end
context 'with HTML' do context 'with existing statuses' do
let(:format) { 'html' } let!(:status) { Fabricate(:status, account: account) }
let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) }
it_behaves_like 'preliminary checks' let!(:status_self_reply) { Fabricate(:status, account: account, thread: status) }
let!(:status_media) { Fabricate(:status, account: account) }
context 'when account is permanently suspended' do let!(:status_pinned) { Fabricate(:status, account: account) }
before do let!(:status_private) { Fabricate(:status, account: account, visibility: :private) }
account.suspend! let!(:status_direct) { Fabricate(:status, account: account, visibility: :direct) }
account.deletion_request.destroy let!(:status_reblog) { Fabricate(:status, account: account, reblog: Fabricate(:status)) }
end
it 'returns http gone' do
get :show, params: { username: account.username, format: format }
expect(response).to have_http_status(410)
end
end
context 'when account is temporarily suspended' do
before do
account.suspend!
end
it 'returns http forbidden' do
get :show, params: { username: account.username, format: format }
expect(response).to have_http_status(403)
end
end
shared_examples 'common response characteristics' do
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns Link header' do
expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account)
end
it 'renders show template' do
expect(response).to render_template(:show)
end
end
context 'with a normal account in an HTML request' do
before do
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common response characteristics'
end
context 'with replies' do
before do
allow(controller).to receive(:replies_requested?).and_return(true)
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common response characteristics'
end
context 'with media' do
before do
allow(controller).to receive(:media_requested?).and_return(true)
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common response characteristics'
end
context 'with tag' do
let(:tag) { Fabricate(:tag) }
let!(:status_tag) { Fabricate(:status, account: account) }
before do
allow(controller).to receive(:tag_requested?).and_return(true)
status_tag.tags << tag
get :show, params: { username: account.username, format: format, tag: tag.to_param }
end
it_behaves_like 'common response characteristics'
end
end
context 'with JSON' do
let(:authorized_fetch_mode) { false }
let(:format) { 'json' }
before do before do
allow(controller).to receive(:authorized_fetch_mode?).and_return(authorized_fetch_mode) status_media.media_attachments << Fabricate(:media_attachment, account: account, type: :image)
account.pinned_statuses << status_pinned
account.pinned_statuses << status_private
end end
it_behaves_like 'preliminary checks' context 'with HTML' do
let(:format) { 'html' }
context 'when account is suspended permanently' do shared_examples 'common HTML response' do
before do it 'returns a standard HTML response', :aggregate_failures do
account.suspend! expect(response).to have_http_status(200)
account.deletion_request.destroy
expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account)
expect(response).to render_template(:show)
end
end end
it 'returns http gone' do context 'with a normal account in an HTML request' do
get :show, params: { username: account.username, format: format } before do
expect(response).to have_http_status(410) get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common HTML response'
end
context 'with replies' do
before do
allow(controller).to receive(:replies_requested?).and_return(true)
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common HTML response'
end
context 'with media' do
before do
allow(controller).to receive(:media_requested?).and_return(true)
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common HTML response'
end
context 'with tag' do
let(:tag) { Fabricate(:tag) }
let!(:status_tag) { Fabricate(:status, account: account) }
before do
allow(controller).to receive(:tag_requested?).and_return(true)
status_tag.tags << tag
get :show, params: { username: account.username, format: format, tag: tag.to_param }
end
it_behaves_like 'common HTML response'
end end
end end
context 'when account is suspended temporarily' do context 'with JSON' do
let(:authorized_fetch_mode) { false }
let(:format) { 'json' }
before do before do
account.suspend! allow(controller).to receive(:authorized_fetch_mode?).and_return(authorized_fetch_mode)
end end
it 'returns http success' do context 'with a normal account in a JSON request' do
get :show, params: { username: account.username, format: format } before do
expect(response).to have_http_status(200) get :show, params: { username: account.username, format: format }
end end
end
context 'with a normal account in a JSON request' do it 'returns a JSON version of the account', :aggregate_failures do
before do expect(response).to have_http_status(200)
get :show, params: { username: account.username, format: format }
expect(response.media_type).to eq 'application/activity+json'
expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
context 'with authorized fetch mode' do
let(:authorized_fetch_mode) { true }
it 'returns http unauthorized' do
expect(response).to have_http_status(401)
end
end
end end
it 'returns http success' do context 'when signed in' do
expect(response).to have_http_status(200) let(:user) { Fabricate(:user) }
before do
sign_in(user)
get :show, params: { username: account.username, format: format }
end
it 'returns a private JSON version of the account', :aggregate_failures 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'
expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end
end end
it 'returns application/activity+json' do context 'with signature' do
expect(response.media_type).to eq 'application/activity+json' let(:remote_account) { Fabricate(:account, domain: 'example.com') }
end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' before do
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
get :show, params: { username: account.username, format: format }
end
it 'renders account' do it 'returns a JSON version of the account', :aggregate_failures do
json = body_as_json expect(response).to have_http_status(200)
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end
context 'with authorized fetch mode' do expect(response.media_type).to eq 'application/activity+json'
let(:authorized_fetch_mode) { true }
it 'returns http unauthorized' do expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
expect(response).to have_http_status(401) end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
context 'with authorized fetch mode' do
let(:authorized_fetch_mode) { true }
it 'returns a private signature JSON version of the account', :aggregate_failures 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'
expect(response.headers['Vary']).to include 'Signature'
expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end
end end
end end
end end
context 'when signed in' do context 'with RSS' do
let(:user) { Fabricate(:user) } let(:format) { 'rss' }
before do
sign_in(user)
get :show, params: { username: account.username, format: format }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns application/activity+json' do
expect(response.media_type).to eq 'application/activity+json'
end
it 'returns private Cache-Control header' do
expect(response.headers['Cache-Control']).to include 'private'
end
it 'renders account' do
json = body_as_json
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end
end
context 'with signature' do
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
before do
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
get :show, params: { username: account.username, format: format }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns application/activity+json' do
expect(response.media_type).to eq 'application/activity+json'
end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'renders account' do
json = body_as_json
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end
context 'with authorized fetch mode' do
let(:authorized_fetch_mode) { true }
shared_examples 'common RSS response' do
it 'returns http success' do it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns application/activity+json' do it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
expect(response.media_type).to eq 'application/activity+json' end
context 'with a normal account in an RSS request' do
before do
get :show, params: { username: account.username, format: format }
end end
it 'returns private Cache-Control header' do it_behaves_like 'common RSS response'
expect(response.headers['Cache-Control']).to include 'private'
end
it 'returns Vary header with Signature' do it 'responds with correct statuses', :aggregate_failures do
expect(response.headers['Vary']).to include 'Signature' expect(response.body).to include_status_tag(status_media)
end expect(response.body).to include_status_tag(status_self_reply)
expect(response.body).to include_status_tag(status)
it 'renders account' do expect(response.body).to_not include_status_tag(status_direct)
json = body_as_json expect(response.body).to_not include_status_tag(status_private)
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) expect(response.body).to_not include_status_tag(status_reblog.reblog)
expect(response.body).to_not include_status_tag(status_reply)
end end
end end
end
end
context 'with RSS' do context 'with replies' do
let(:format) { 'rss' } before do
allow(controller).to receive(:replies_requested?).and_return(true)
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'preliminary checks' it_behaves_like 'common RSS response'
context 'when account is permanently suspended' do it 'responds with correct statuses with replies', :aggregate_failures do
before do expect(response.body).to include_status_tag(status_media)
account.suspend! expect(response.body).to include_status_tag(status_reply)
account.deletion_request.destroy expect(response.body).to include_status_tag(status_self_reply)
expect(response.body).to include_status_tag(status)
expect(response.body).to_not include_status_tag(status_direct)
expect(response.body).to_not include_status_tag(status_private)
expect(response.body).to_not include_status_tag(status_reblog.reblog)
end
end end
it 'returns http gone' do context 'with media' do
get :show, params: { username: account.username, format: format } before do
expect(response).to have_http_status(410) allow(controller).to receive(:media_requested?).and_return(true)
end get :show, params: { username: account.username, format: format }
end end
context 'when account is temporarily suspended' do it_behaves_like 'common RSS response'
before do
account.suspend! it 'responds with correct statuses with media', :aggregate_failures do
expect(response.body).to include_status_tag(status_media)
expect(response.body).to_not include_status_tag(status_direct)
expect(response.body).to_not include_status_tag(status_private)
expect(response.body).to_not include_status_tag(status_reblog.reblog)
expect(response.body).to_not include_status_tag(status_reply)
expect(response.body).to_not include_status_tag(status_self_reply)
expect(response.body).to_not include_status_tag(status)
end
end end
it 'returns http forbidden' do context 'with tag' do
get :show, params: { username: account.username, format: format } let(:tag) { Fabricate(:tag) }
expect(response).to have_http_status(403)
end
end
shared_examples 'common response characteristics' do let!(:status_tag) { Fabricate(:status, account: account) }
it 'returns http success' do
expect(response).to have_http_status(200)
end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' before do
end allow(controller).to receive(:tag_requested?).and_return(true)
status_tag.tags << tag
get :show, params: { username: account.username, format: format, tag: tag.to_param }
end
context 'with a normal account in an RSS request' do it_behaves_like 'common RSS response'
before do
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common response characteristics' it 'responds with correct statuses with a tag', :aggregate_failures do
expect(response.body).to include_status_tag(status_tag)
it 'renders public status' do expect(response.body).to_not include_status_tag(status_direct)
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) expect(response.body).to_not include_status_tag(status_media)
end expect(response.body).to_not include_status_tag(status_private)
expect(response.body).to_not include_status_tag(status_reblog.reblog)
it 'renders self-reply' do expect(response.body).to_not include_status_tag(status_reply)
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply)) expect(response.body).to_not include_status_tag(status_self_reply)
end expect(response.body).to_not include_status_tag(status)
end
it 'renders status with media' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'does not render reblog' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'does not render reply to someone else' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
end
end
context 'with replies' do
before do
allow(controller).to receive(:replies_requested?).and_return(true)
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common response characteristics'
it 'renders public status' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status))
end
it 'renders self-reply' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply))
end
it 'renders status with media' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'does not render reblog' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'renders reply to someone else' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reply))
end
end
context 'with media' do
before do
allow(controller).to receive(:media_requested?).and_return(true)
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common response characteristics'
it 'does not render public status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
end
it 'does not render self-reply' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
end
it 'renders status with media' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'does not render reblog' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'does not render reply to someone else' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
end
end
context 'with tag' do
let(:tag) { Fabricate(:tag) }
let!(:status_tag) { Fabricate(:status, account: account) }
before do
allow(controller).to receive(:tag_requested?).and_return(true)
status_tag.tags << tag
get :show, params: { username: account.username, format: format, tag: tag.to_param }
end
it_behaves_like 'common response characteristics'
it 'does not render public status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
end
it 'does not render self-reply' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
end
it 'does not render status with media' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'does not render reblog' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'does not render reply to someone else' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
end
it 'renders status with tag' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_tag))
end end
end end
end end
end end
def include_status_tag(status)
include ActivityPub::TagManager.instance.url_for(status)
end
end end

View file

@ -22,7 +22,7 @@ describe 'Admin::Accounts' do
context 'without selecting any accounts' do context 'without selecting any accounts' do
it 'displays a notice about account selection' do it 'displays a notice about account selection' do
click_on button_for_suspend click_button button_for_suspend
expect(page).to have_content(selection_error_text) expect(page).to have_content(selection_error_text)
end end
@ -32,7 +32,7 @@ describe 'Admin::Accounts' do
it 'suspends the account' do it 'suspends the account' do
batch_checkbox_for(approved_user_account).check batch_checkbox_for(approved_user_account).check
click_on button_for_suspend click_button button_for_suspend
expect(approved_user_account.reload).to be_suspended expect(approved_user_account.reload).to be_suspended
end end
@ -42,7 +42,7 @@ describe 'Admin::Accounts' do
it 'approves the account user' do it 'approves the account user' do
batch_checkbox_for(unapproved_user_account).check batch_checkbox_for(unapproved_user_account).check
click_on button_for_approve click_button button_for_approve
expect(unapproved_user_account.reload.user).to be_approved expect(unapproved_user_account.reload.user).to be_approved
end end
@ -52,7 +52,7 @@ describe 'Admin::Accounts' do
it 'rejects and removes the account' do it 'rejects and removes the account' do
batch_checkbox_for(unapproved_user_account).check batch_checkbox_for(unapproved_user_account).check
click_on button_for_reject click_button button_for_reject
expect { unapproved_user_account.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { unapproved_user_account.reload }.to raise_error(ActiveRecord::RecordNotFound)
end end

View file

@ -16,7 +16,7 @@ describe 'Admin::CustomEmojis' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' do it 'displays a notice about selection' do
click_on button_for_enable click_button button_for_enable
expect(page).to have_content(selection_error_text) expect(page).to have_content(selection_error_text)
end end

View file

@ -13,7 +13,7 @@ describe 'blocking domains through the moderation interface' do
fill_in 'domain_block_domain', with: 'example.com' fill_in 'domain_block_domain', with: 'example.com'
select I18n.t('admin.domain_blocks.new.severity.silence'), from: 'domain_block_severity' select I18n.t('admin.domain_blocks.new.severity.silence'), from: 'domain_block_severity'
click_on I18n.t('admin.domain_blocks.new.create') click_button I18n.t('admin.domain_blocks.new.create')
expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be true expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be true
end end
@ -25,13 +25,13 @@ describe 'blocking domains through the moderation interface' do
fill_in 'domain_block_domain', with: 'example.com' fill_in 'domain_block_domain', with: 'example.com'
select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity'
click_on I18n.t('admin.domain_blocks.new.create') click_button I18n.t('admin.domain_blocks.new.create')
# It presents a confirmation screen # It presents a confirmation screen
expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com'))
# Confirming creates a block # Confirming creates a block
click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') click_button I18n.t('admin.domain_blocks.confirm_suspension.confirm')
expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true
end end
@ -45,13 +45,13 @@ describe 'blocking domains through the moderation interface' do
fill_in 'domain_block_domain', with: 'example.com' fill_in 'domain_block_domain', with: 'example.com'
select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity'
click_on I18n.t('admin.domain_blocks.new.create') click_button I18n.t('admin.domain_blocks.new.create')
# It presents a confirmation screen # It presents a confirmation screen
expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com'))
# Confirming updates the block # Confirming updates the block
click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') click_button I18n.t('admin.domain_blocks.confirm_suspension.confirm')
expect(domain_block.reload.severity).to eq 'suspend' expect(domain_block.reload.severity).to eq 'suspend'
end end
@ -65,13 +65,13 @@ describe 'blocking domains through the moderation interface' do
fill_in 'domain_block_domain', with: 'subdomain.example.com' fill_in 'domain_block_domain', with: 'subdomain.example.com'
select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity'
click_on I18n.t('admin.domain_blocks.new.create') click_button I18n.t('admin.domain_blocks.new.create')
# It presents a confirmation screen # It presents a confirmation screen
expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'subdomain.example.com')) expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'subdomain.example.com'))
# Confirming creates the block # Confirming creates the block
click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') click_button I18n.t('admin.domain_blocks.confirm_suspension.confirm')
expect(DomainBlock.where(domain: 'subdomain.example.com', severity: 'suspend')).to exist expect(DomainBlock.where(domain: 'subdomain.example.com', severity: 'suspend')).to exist
@ -88,13 +88,13 @@ describe 'blocking domains through the moderation interface' do
visit edit_admin_domain_block_path(domain_block) visit edit_admin_domain_block_path(domain_block)
select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity'
click_on I18n.t('generic.save_changes') click_button I18n.t('generic.save_changes')
# It presents a confirmation screen # It presents a confirmation screen
expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com'))
# Confirming updates the block # Confirming updates the block
click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') click_button I18n.t('admin.domain_blocks.confirm_suspension.confirm')
expect(domain_block.reload.severity).to eq 'suspend' expect(domain_block.reload.severity).to eq 'suspend'
end end

View file

@ -16,7 +16,7 @@ describe 'Admin::EmailDomainBlocks' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' do it 'displays a notice about selection' do
click_on button_for_delete click_button button_for_delete
expect(page).to have_content(selection_error_text) expect(page).to have_content(selection_error_text)
end end

View file

@ -16,7 +16,7 @@ describe 'Admin::IpBlocks' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' do it 'displays a notice about selection' do
click_on button_for_delete click_button button_for_delete
expect(page).to have_content(selection_error_text) expect(page).to have_content(selection_error_text)
end end

View file

@ -11,13 +11,13 @@ describe 'finding software updates through the admin interface' do
it 'shows a link to the software updates page, which links to release notes' do it 'shows a link to the software updates page, which links to release notes' do
visit settings_profile_path visit settings_profile_path
click_on I18n.t('admin.critical_update_pending') click_link I18n.t('admin.critical_update_pending')
expect(page).to have_title(I18n.t('admin.software_updates.title')) expect(page).to have_title(I18n.t('admin.software_updates.title'))
expect(page).to have_content('99.99.99') expect(page).to have_content('99.99.99')
click_on I18n.t('admin.software_updates.release_notes') click_link I18n.t('admin.software_updates.release_notes')
expect(page).to have_current_path('https://github.com/mastodon/mastodon/releases/v99', url: true) expect(page).to have_current_path('https://github.com/mastodon/mastodon/releases/v99', url: true)
end end
end end

View file

@ -17,7 +17,7 @@ describe 'Admin::Statuses' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' do it 'displays a notice about selection' do
click_on button_for_report click_button button_for_report
expect(page).to have_content(selection_error_text) expect(page).to have_content(selection_error_text)
end end

View file

@ -16,7 +16,7 @@ describe 'Admin::Trends::Links::PreviewCardProviders' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' do it 'displays a notice about selection' do
click_on button_for_allow click_button button_for_allow
expect(page).to have_content(selection_error_text) expect(page).to have_content(selection_error_text)
end end

View file

@ -16,7 +16,7 @@ describe 'Admin::Trends::Links' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' do it 'displays a notice about selection' do
click_on button_for_allow click_button button_for_allow
expect(page).to have_content(selection_error_text) expect(page).to have_content(selection_error_text)
end end

View file

@ -16,7 +16,7 @@ describe 'Admin::Trends::Statuses' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' do it 'displays a notice about selection' do
click_on button_for_allow click_button button_for_allow
expect(page).to have_content(selection_error_text) expect(page).to have_content(selection_error_text)
end end

View file

@ -16,7 +16,7 @@ describe 'Admin::Trends::Tags' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' do it 'displays a notice about selection' do
click_on button_for_allow click_button button_for_allow
expect(page).to have_content(selection_error_text) expect(page).to have_content(selection_error_text)
end end

View file

@ -27,7 +27,7 @@ describe 'email confirmation flow when captcha is enabled' do
expect(user.reload.confirmed?).to be false expect(user.reload.confirmed?).to be false
# It redirects to app and confirms user # It redirects to app and confirms user
click_on I18n.t('challenge.confirm') click_button I18n.t('challenge.confirm')
expect(user.reload.confirmed?).to be true expect(user.reload.confirmed?).to be true
expect(page).to have_current_path(/\A#{client_app.confirmation_redirect_uri}/, url: true) expect(page).to have_current_path(/\A#{client_app.confirmation_redirect_uri}/, url: true)
end end

View file

@ -19,7 +19,7 @@ describe 'Log in' do
it 'A valid email and password user is able to log in' do it 'A valid email and password user is able to log in' do
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password fill_in 'user_password', with: password
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(subject).to have_css('div.app-holder') expect(subject).to have_css('div.app-holder')
end end
@ -27,7 +27,7 @@ describe 'Log in' do
it 'A invalid email and password user is not able to log in' do it 'A invalid email and password user is not able to log in' do
fill_in 'user_email', with: 'invalid_email' fill_in 'user_email', with: 'invalid_email'
fill_in 'user_password', with: 'invalid_password' fill_in 'user_password', with: 'invalid_password'
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(subject).to have_css('.flash-message', text: failure_message('invalid')) expect(subject).to have_css('.flash-message', text: failure_message('invalid'))
end end
@ -38,7 +38,7 @@ describe 'Log in' do
it 'A unconfirmed user is able to log in' do it 'A unconfirmed user is able to log in' do
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password fill_in 'user_password', with: password
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(subject).to have_css('div.admin-wrapper') expect(subject).to have_css('div.admin-wrapper')
end end

View file

@ -20,7 +20,7 @@ describe 'Using OAuth from an external app' do
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
# Upon authorizing, it redirects to the apps' callback URL # Upon authorizing, it redirects to the apps' callback URL
click_on I18n.t('doorkeeper.authorizations.buttons.authorize') click_button I18n.t('doorkeeper.authorizations.buttons.authorize')
expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true)
# It grants the app access to the account # It grants the app access to the account
@ -35,7 +35,7 @@ describe 'Using OAuth from an external app' do
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.deny')) expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.deny'))
# Upon denying, it redirects to the apps' callback URL # Upon denying, it redirects to the apps' callback URL
click_on I18n.t('doorkeeper.authorizations.buttons.deny') click_button I18n.t('doorkeeper.authorizations.buttons.deny')
expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true)
# It does not grant the app access to the account # It does not grant the app access to the account
@ -63,17 +63,17 @@ describe 'Using OAuth from an external app' do
# Failing to log-in presents the form again # Failing to log-in presents the form again
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: 'wrong password' fill_in 'user_password', with: 'wrong password'
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(page).to have_content(I18n.t('auth.login')) expect(page).to have_content(I18n.t('auth.login'))
# Logging in redirects to an authorization page # Logging in redirects to an authorization page
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password fill_in 'user_password', with: password
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
# Upon authorizing, it redirects to the apps' callback URL # Upon authorizing, it redirects to the apps' callback URL
click_on I18n.t('doorkeeper.authorizations.buttons.authorize') click_button I18n.t('doorkeeper.authorizations.buttons.authorize')
expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true)
# It grants the app access to the account # It grants the app access to the account
@ -90,17 +90,17 @@ describe 'Using OAuth from an external app' do
# Failing to log-in presents the form again # Failing to log-in presents the form again
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: 'wrong password' fill_in 'user_password', with: 'wrong password'
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(page).to have_content(I18n.t('auth.login')) expect(page).to have_content(I18n.t('auth.login'))
# Logging in redirects to an authorization page # Logging in redirects to an authorization page
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password fill_in 'user_password', with: password
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
# Upon denying, it redirects to the apps' callback URL # Upon denying, it redirects to the apps' callback URL
click_on I18n.t('doorkeeper.authorizations.buttons.deny') click_button I18n.t('doorkeeper.authorizations.buttons.deny')
expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true)
# It does not grant the app access to the account # It does not grant the app access to the account
@ -120,27 +120,27 @@ describe 'Using OAuth from an external app' do
# Failing to log-in presents the form again # Failing to log-in presents the form again
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: 'wrong password' fill_in 'user_password', with: 'wrong password'
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(page).to have_content(I18n.t('auth.login')) expect(page).to have_content(I18n.t('auth.login'))
# Logging in redirects to a two-factor authentication page # Logging in redirects to a two-factor authentication page
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password fill_in 'user_password', with: password
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
# Filling in an incorrect two-factor authentication code presents the form again # Filling in an incorrect two-factor authentication code presents the form again
fill_in 'user_otp_attempt', with: 'wrong' fill_in 'user_otp_attempt', with: 'wrong'
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
# Filling in the correct TOTP code redirects to an app authorization page # Filling in the correct TOTP code redirects to an app authorization page
fill_in 'user_otp_attempt', with: user.current_otp fill_in 'user_otp_attempt', with: user.current_otp
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
# Upon authorizing, it redirects to the apps' callback URL # Upon authorizing, it redirects to the apps' callback URL
click_on I18n.t('doorkeeper.authorizations.buttons.authorize') click_button I18n.t('doorkeeper.authorizations.buttons.authorize')
expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true)
# It grants the app access to the account # It grants the app access to the account
@ -157,27 +157,27 @@ describe 'Using OAuth from an external app' do
# Failing to log-in presents the form again # Failing to log-in presents the form again
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: 'wrong password' fill_in 'user_password', with: 'wrong password'
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(page).to have_content(I18n.t('auth.login')) expect(page).to have_content(I18n.t('auth.login'))
# Logging in redirects to a two-factor authentication page # Logging in redirects to a two-factor authentication page
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password fill_in 'user_password', with: password
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
# Filling in an incorrect two-factor authentication code presents the form again # Filling in an incorrect two-factor authentication code presents the form again
fill_in 'user_otp_attempt', with: 'wrong' fill_in 'user_otp_attempt', with: 'wrong'
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
# Filling in the correct TOTP code redirects to an app authorization page # Filling in the correct TOTP code redirects to an app authorization page
fill_in 'user_otp_attempt', with: user.current_otp fill_in 'user_otp_attempt', with: user.current_otp
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
# Upon denying, it redirects to the apps' callback URL # Upon denying, it redirects to the apps' callback URL
click_on I18n.t('doorkeeper.authorizations.buttons.deny') click_button I18n.t('doorkeeper.authorizations.buttons.deny')
expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true)
# It does not grant the app access to the account # It does not grant the app access to the account

View file

@ -6,6 +6,24 @@ require 'mastodon/cli/accounts'
describe Mastodon::CLI::Accounts do describe Mastodon::CLI::Accounts do
let(:cli) { described_class.new } let(:cli) { described_class.new }
# `parallelize_with_progress` cannot run in transactions, so instead,
# stub it with an alternative implementation that runs sequentially
# and can run in transactions.
def stub_parallelize_with_progress!
allow(cli).to receive(:parallelize_with_progress) do |scope, &block|
aggregate = 0
total = 0
scope.reorder(nil).find_each do |record|
value = block.call(record)
aggregate += value if value.is_a?(Integer)
total += 1
end
[total, aggregate]
end
end
describe '.exit_on_failure?' do describe '.exit_on_failure?' do
it 'returns true' do it 'returns true' do
expect(described_class.exit_on_failure?).to be true expect(described_class.exit_on_failure?).to be true
@ -551,20 +569,15 @@ describe Mastodon::CLI::Accounts do
let!(:follower_rony) { Fabricate(:account, username: 'rony') } let!(:follower_rony) { Fabricate(:account, username: 'rony') }
let!(:follower_charles) { Fabricate(:account, username: 'charles') } let!(:follower_charles) { Fabricate(:account, username: 'charles') }
let(:follow_service) { instance_double(FollowService, call: nil) } let(:follow_service) { instance_double(FollowService, call: nil) }
let(:scope) { Account.local.without_suspended }
before do before do
allow(cli).to receive(:parallelize_with_progress).and_yield(follower_bob)
.and_yield(follower_rony)
.and_yield(follower_charles)
.and_return([3, nil])
allow(FollowService).to receive(:new).and_return(follow_service) allow(FollowService).to receive(:new).and_return(follow_service)
stub_parallelize_with_progress!
end end
it 'makes all local accounts follow the target account' do it 'makes all local accounts follow the target account' do
cli.follow(target_account.username) cli.follow(target_account.username)
expect(cli).to have_received(:parallelize_with_progress).with(scope).once
expect(follow_service).to have_received(:call).with(follower_bob, target_account, any_args).once expect(follow_service).to have_received(:call).with(follower_bob, target_account, any_args).once
expect(follow_service).to have_received(:call).with(follower_rony, target_account, any_args).once expect(follow_service).to have_received(:call).with(follower_rony, target_account, any_args).once
expect(follow_service).to have_received(:call).with(follower_charles, target_account, any_args).once expect(follow_service).to have_received(:call).with(follower_charles, target_account, any_args).once
@ -572,7 +585,7 @@ describe Mastodon::CLI::Accounts do
it 'displays a successful message' do it 'displays a successful message' do
expect { cli.follow(target_account.username) }.to output( expect { cli.follow(target_account.username) }.to output(
a_string_including('OK, followed target from 3 accounts') a_string_including("OK, followed target from #{Account.local.count} accounts")
).to_stdout ).to_stdout
end end
end end
@ -592,26 +605,21 @@ describe Mastodon::CLI::Accounts do
context 'when the given username is found' do context 'when the given username is found' do
let!(:target_account) { Fabricate(:account) } let!(:target_account) { Fabricate(:account) }
let!(:follower_chris) { Fabricate(:account, username: 'chris') } let!(:follower_chris) { Fabricate(:account, username: 'chris', domain: nil) }
let!(:follower_rambo) { Fabricate(:account, username: 'rambo') } let!(:follower_rambo) { Fabricate(:account, username: 'rambo', domain: nil) }
let!(:follower_ana) { Fabricate(:account, username: 'ana') } let!(:follower_ana) { Fabricate(:account, username: 'ana', domain: nil) }
let(:unfollow_service) { instance_double(UnfollowService, call: nil) } let(:unfollow_service) { instance_double(UnfollowService, call: nil) }
let(:scope) { target_account.followers.local }
before do before do
accounts = [follower_chris, follower_rambo, follower_ana] accounts = [follower_chris, follower_rambo, follower_ana]
accounts.each { |account| target_account.follow!(account) } accounts.each { |account| account.follow!(target_account) }
allow(cli).to receive(:parallelize_with_progress).and_yield(follower_chris)
.and_yield(follower_rambo)
.and_yield(follower_ana)
.and_return([3, nil])
allow(UnfollowService).to receive(:new).and_return(unfollow_service) allow(UnfollowService).to receive(:new).and_return(unfollow_service)
stub_parallelize_with_progress!
end end
it 'makes all local accounts unfollow the target account' do it 'makes all local accounts unfollow the target account' do
cli.unfollow(target_account.username) cli.unfollow(target_account.username)
expect(cli).to have_received(:parallelize_with_progress).with(scope).once
expect(unfollow_service).to have_received(:call).with(follower_chris, target_account).once expect(unfollow_service).to have_received(:call).with(follower_chris, target_account).once
expect(unfollow_service).to have_received(:call).with(follower_rambo, target_account).once expect(unfollow_service).to have_received(:call).with(follower_rambo, target_account).once
expect(unfollow_service).to have_received(:call).with(follower_ana, target_account).once expect(unfollow_service).to have_received(:call).with(follower_ana, target_account).once
@ -671,6 +679,8 @@ describe Mastodon::CLI::Accounts do
let(:scope) { Account.remote } let(:scope) { Account.remote }
before do before do
# TODO: we should be using `stub_parallelize_with_progress!` but
# this makes the assertions harder to write
allow(cli).to receive(:parallelize_with_progress).and_yield(remote_account_example_com) allow(cli).to receive(:parallelize_with_progress).and_yield(remote_account_example_com)
.and_yield(account_example_net) .and_yield(account_example_net)
.and_return([2, nil]) .and_return([2, nil])
@ -1112,26 +1122,19 @@ describe Mastodon::CLI::Accounts do
describe '#cull' do describe '#cull' do
let(:delete_account_service) { instance_double(DeleteAccountService, call: nil) } let(:delete_account_service) { instance_double(DeleteAccountService, call: nil) }
let!(:tom) { Fabricate(:account, updated_at: 30.days.ago, username: 'tom', uri: 'https://example.com/users/tom', domain: 'example.com') } let!(:tom) { Fabricate(:account, updated_at: 30.days.ago, username: 'tom', uri: 'https://example.com/users/tom', domain: 'example.com', protocol: :activitypub) }
let!(:bob) { Fabricate(:account, updated_at: 30.days.ago, last_webfingered_at: nil, username: 'bob', uri: 'https://example.org/users/bob', domain: 'example.org') } let!(:bob) { Fabricate(:account, updated_at: 30.days.ago, last_webfingered_at: nil, username: 'bob', uri: 'https://example.org/users/bob', domain: 'example.org', protocol: :activitypub) }
let!(:gon) { Fabricate(:account, updated_at: 15.days.ago, last_webfingered_at: 15.days.ago, username: 'gon', uri: 'https://example.net/users/gon', domain: 'example.net') } let!(:gon) { Fabricate(:account, updated_at: 15.days.ago, last_webfingered_at: 15.days.ago, username: 'gon', uri: 'https://example.net/users/gon', domain: 'example.net', protocol: :activitypub) }
let!(:ana) { Fabricate(:account, username: 'ana', uri: 'https://example.com/users/ana', domain: 'example.com') } let!(:ana) { Fabricate(:account, username: 'ana', uri: 'https://example.com/users/ana', domain: 'example.com', protocol: :activitypub) }
let!(:tales) { Fabricate(:account, updated_at: 10.days.ago, last_webfingered_at: nil, username: 'tales', uri: 'https://example.net/users/tales', domain: 'example.net') } let!(:tales) { Fabricate(:account, updated_at: 10.days.ago, last_webfingered_at: nil, username: 'tales', uri: 'https://example.net/users/tales', domain: 'example.net', protocol: :activitypub) }
before do before do
allow(DeleteAccountService).to receive(:new).and_return(delete_account_service) allow(DeleteAccountService).to receive(:new).and_return(delete_account_service)
end end
context 'when no domain is specified' do context 'when no domain is specified' do
let(:scope) { Account.remote.where(protocol: :activitypub).partitioned }
before do before do
allow(cli).to receive(:parallelize_with_progress).and_yield(tom) stub_parallelize_with_progress!
.and_yield(bob)
.and_yield(gon)
.and_yield(ana)
.and_yield(tales)
.and_return([5, 3])
stub_request(:head, 'https://example.org/users/bob').to_return(status: 404) stub_request(:head, 'https://example.org/users/bob').to_return(status: 404)
stub_request(:head, 'https://example.net/users/gon').to_return(status: 410) stub_request(:head, 'https://example.net/users/gon').to_return(status: 410)
stub_request(:head, 'https://example.net/users/tales').to_return(status: 200) stub_request(:head, 'https://example.net/users/tales').to_return(status: 200)
@ -1140,7 +1143,6 @@ describe Mastodon::CLI::Accounts do
it 'deletes all inactive remote accounts that longer exist in the origin server' do it 'deletes all inactive remote accounts that longer exist in the origin server' do
cli.cull cli.cull
expect(cli).to have_received(:parallelize_with_progress).with(scope).once
expect(delete_account_service).to have_received(:call).with(bob, reserve_username: false).once expect(delete_account_service).to have_received(:call).with(bob, reserve_username: false).once
expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once
end end
@ -1148,35 +1150,27 @@ describe Mastodon::CLI::Accounts do
it 'does not delete any active remote account that still exists in the origin server' do it 'does not delete any active remote account that still exists in the origin server' do
cli.cull cli.cull
expect(cli).to have_received(:parallelize_with_progress).with(scope).once
expect(delete_account_service).to_not have_received(:call).with(tom, reserve_username: false) expect(delete_account_service).to_not have_received(:call).with(tom, reserve_username: false)
expect(delete_account_service).to_not have_received(:call).with(ana, reserve_username: false) expect(delete_account_service).to_not have_received(:call).with(ana, reserve_username: false)
expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false) expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false)
end end
it 'touches inactive remote accounts that have not been deleted' do it 'touches inactive remote accounts that have not been deleted' do
allow(tales).to receive(:touch) expect { cli.cull }.to(change { tales.reload.updated_at })
cli.cull
expect(tales).to have_received(:touch).once
end end
it 'displays the summary correctly' do it 'displays the summary correctly' do
expect { cli.cull }.to output( expect { cli.cull }.to output(
a_string_including('Visited 5 accounts, removed 3') a_string_including('Visited 5 accounts, removed 2')
).to_stdout ).to_stdout
end end
end end
context 'when a domain is specified' do context 'when a domain is specified' do
let(:domain) { 'example.net' } let(:domain) { 'example.net' }
let(:scope) { Account.remote.where(protocol: :activitypub, domain: domain).partitioned }
before do before do
allow(cli).to receive(:parallelize_with_progress).and_yield(gon) stub_parallelize_with_progress!
.and_yield(tales)
.and_return([2, 2])
stub_request(:head, 'https://example.net/users/gon').to_return(status: 410) stub_request(:head, 'https://example.net/users/gon').to_return(status: 410)
stub_request(:head, 'https://example.net/users/tales').to_return(status: 404) stub_request(:head, 'https://example.net/users/tales').to_return(status: 404)
end end
@ -1184,13 +1178,12 @@ describe Mastodon::CLI::Accounts do
it 'deletes inactive remote accounts that longer exist in the specified domain' do it 'deletes inactive remote accounts that longer exist in the specified domain' do
cli.cull(domain) cli.cull(domain)
expect(cli).to have_received(:parallelize_with_progress).with(scope).once
expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once
expect(delete_account_service).to have_received(:call).with(tales, reserve_username: false).once expect(delete_account_service).to have_received(:call).with(tales, reserve_username: false).once
end end
it 'displays the summary correctly' do it 'displays the summary correctly' do
expect { cli.cull }.to output( expect { cli.cull(domain) }.to output(
a_string_including('Visited 2 accounts, removed 2') a_string_including('Visited 2 accounts, removed 2')
).to_stdout ).to_stdout
end end
@ -1199,7 +1192,9 @@ describe Mastodon::CLI::Accounts do
context 'when a domain is unavailable' do context 'when a domain is unavailable' do
shared_examples 'an unavailable domain' do shared_examples 'an unavailable domain' do
before do before do
allow(cli).to receive(:parallelize_with_progress).and_yield(tales).and_return([1, 0]) stub_parallelize_with_progress!
stub_request(:head, 'https://example.org/users/bob').to_return(status: 200)
stub_request(:head, 'https://example.net/users/gon').to_return(status: 200)
end end
it 'skips accounts from the unavailable domain' do it 'skips accounts from the unavailable domain' do
@ -1210,7 +1205,7 @@ describe Mastodon::CLI::Accounts do
it 'displays the summary correctly' do it 'displays the summary correctly' do
expect { cli.cull }.to output( expect { cli.cull }.to output(
a_string_including("Visited 1 accounts, removed 0\nThe following domains were not available during the check:\n example.net") a_string_including("Visited 5 accounts, removed 0\nThe following domains were not available during the check:\n example.net")
).to_stdout ).to_stdout
end end
end end

View file

@ -4,9 +4,52 @@ require 'rails_helper'
require 'mastodon/cli/preview_cards' require 'mastodon/cli/preview_cards'
describe Mastodon::CLI::PreviewCards do describe Mastodon::CLI::PreviewCards do
let(:cli) { described_class.new }
describe '.exit_on_failure?' do describe '.exit_on_failure?' do
it 'returns true' do it 'returns true' do
expect(described_class.exit_on_failure?).to be true expect(described_class.exit_on_failure?).to be true
end end
end end
describe '#remove' do
context 'with relevant preview cards' do
before do
Fabricate(:preview_card, updated_at: 10.years.ago, type: :link)
Fabricate(:preview_card, updated_at: 10.months.ago, type: :photo)
Fabricate(:preview_card, updated_at: 10.days.ago, type: :photo)
end
context 'with no arguments' do
it 'deletes thumbnails for local preview cards' do
expect { cli.invoke(:remove) }.to output(
a_string_including('Removed 2 preview cards')
.and(a_string_including('approx. 119 KB'))
).to_stdout
end
end
context 'with the --link option' do
let(:options) { { link: true } }
it 'deletes thumbnails for local preview cards' do
expect { cli.invoke(:remove, [], options) }.to output(
a_string_including('Removed 1 link-type preview cards')
.and(a_string_including('approx. 59.6 KB'))
).to_stdout
end
end
context 'with the --days option' do
let(:options) { { days: 365 } }
it 'deletes thumbnails for local preview cards' do
expect { cli.invoke(:remove, [], options) }.to output(
a_string_including('Removed 1 preview cards')
.and(a_string_including('approx. 59.6 KB'))
).to_stdout
end
end
end
end
end end

View file

@ -29,4 +29,23 @@ describe Poll do
end end
end end
end end
describe 'validations' do
context 'when valid' do
let(:poll) { Fabricate.build(:poll) }
it 'is valid with valid attributes' do
expect(poll).to be_valid
end
end
context 'when not valid' do
let(:poll) { Fabricate.build(:poll, expires_at: nil) }
it 'is invalid without an expire date' do
poll.valid?
expect(poll).to model_have_error_on_field(:expires_at)
end
end
end
end end

View file

@ -42,6 +42,12 @@ RSpec.configure do |config|
# for RSpec::Retry # for RSpec::Retry
config.verbose_retry = true config.verbose_retry = true
config.display_try_failure_messages = true config.display_try_failure_messages = true
# Use the GitHub Annotations formatter for CI
if ENV['GITHUB_ACTIONS'] == 'true'
require 'rspec/github'
config.add_formatter RSpec::Github::Formatter
end
end end
def body_as_json def body_as_json

View file

@ -18,7 +18,7 @@ module ProfileStories
visit new_user_session_path visit new_user_session_path
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password fill_in 'user_password', with: password
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
end end
def with_alice_as_local_user def with_alice_as_local_user

View file

@ -24,10 +24,10 @@ describe 'NewStatuses' do
within('.compose-form') do within('.compose-form') do
fill_in "What's on your mind?", with: status_text fill_in "What's on your mind?", with: status_text
click_on 'Publish!' click_button 'Publish!'
end end
expect(subject).to have_selector('.status__content__text', text: status_text) expect(subject).to have_css('.status__content__text', text: status_text)
end end
it 'can be posted again' do it 'can be posted again' do
@ -37,9 +37,9 @@ describe 'NewStatuses' do
within('.compose-form') do within('.compose-form') do
fill_in "What's on your mind?", with: status_text fill_in "What's on your mind?", with: status_text
click_on 'Publish!' click_button 'Publish!'
end end
expect(subject).to have_selector('.status__content__text', text: status_text) expect(subject).to have_css('.status__content__text', text: status_text)
end end
end end

View file

@ -2,41 +2,118 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe UnreservedUsernameValidator, type: :validator do describe UnreservedUsernameValidator do
describe '#validate' do let(:record_class) do
before do Class.new do
allow(validator).to receive(:reserved_username?) { reserved_username } include ActiveModel::Validations
validator.validate(account) attr_accessor :username
validates_with UnreservedUsernameValidator
end end
end
let(:record) { record_class.new }
let(:validator) { described_class.new } describe '#validate' do
let(:account) { instance_double(Account, username: username, errors: errors) } context 'when username is nil' do
let(:errors) { instance_double(ActiveModel::Errors, add: nil) } it 'does not add errors' do
record.username = nil
context 'when @username is blank?' do expect(record).to be_valid
let(:username) { nil } expect(record.errors).to be_empty
it 'not calls errors.add' do
expect(errors).to_not have_received(:add).with(:username, any_args)
end end
end end
context 'when @username is not blank?' do context 'when PAM is enabled' do
let(:username) { 'f' } before do
allow(Devise).to receive(:pam_authentication).and_return(true)
end
context 'with reserved_username?' do context 'with a pam service available' do
let(:reserved_username) { true } let(:service) { double }
let(:pam_class) do
Class.new do
def self.account(service, username); end
end
end
it 'calls errors.add' do before do
expect(errors).to have_received(:add).with(:username, :reserved) stub_const('Rpam2', pam_class)
allow(Devise).to receive(:pam_controlled_service).and_return(service)
end
context 'when the account exists' do
before do
allow(Rpam2).to receive(:account).with(service, 'username').and_return(true)
end
it 'adds errors to the record' do
record.username = 'username'
expect(record).to_not be_valid
expect(record.errors.first.attribute).to eq(:username)
expect(record.errors.first.type).to eq(:reserved)
end
end
context 'when the account does not exist' do
before do
allow(Rpam2).to receive(:account).with(service, 'username').and_return(false)
end
it 'does not add errors to the record' do
record.username = 'username'
expect(record).to be_valid
expect(record.errors).to be_empty
end
end end
end end
context 'when username is not reserved' do context 'without a pam service' do
let(:reserved_username) { false } before do
allow(Devise).to receive(:pam_controlled_service).and_return(false)
end
it 'not calls errors.add' do context 'when there are not any reserved usernames' do
expect(errors).to_not have_received(:add).with(:username, any_args) before do
stub_reserved_usernames(nil)
end
it 'does not add errors to the record' do
record.username = 'username'
expect(record).to be_valid
expect(record.errors).to be_empty
end
end
context 'when there are reserved usernames' do
before do
stub_reserved_usernames(%w(alice bob))
end
context 'when the username is reserved' do
it 'adds errors to the record' do
record.username = 'alice'
expect(record).to_not be_valid
expect(record.errors.first.attribute).to eq(:username)
expect(record.errors.first.type).to eq(:reserved)
end
end
context 'when the username is not reserved' do
it 'does not add errors to the record' do
record.username = 'chris'
expect(record).to be_valid
expect(record.errors).to be_empty
end
end
end
def stub_reserved_usernames(value)
allow(Setting).to receive(:[]).with('reserved_usernames').and_return(value)
end end
end end
end end