Merge commit 'c9ea91f8683cd5c0cfac14071a17e3956ac6d3b0' into glitch-soc/merge-upstream

Conflicts:
- `spec/requests/api/v1/timelines/tag_spec.rb`:
  Upstream refactored this file, while we had a change to switch a default setting.
  Updated as upstream did.
- `spec/views/statuses/show.html.haml_spec.rb`:
  Upstream refactored this file, while we stubbed different methods.
  Updated as upstream did, and updated the stubs accordingly.
This commit is contained in:
Claire 2024-09-03 21:51:53 +02:00
commit fef055db96
87 changed files with 851 additions and 477 deletions

3
.gitignore vendored
View File

@ -71,3 +71,6 @@ docker-compose.override.yml
# Ignore dotenv .local files
.env*.local
# Ignore local-only rspec configuration
.rspec-local

View File

@ -1 +0,0 @@
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio:/app/.apt/usr/lib/x86_64-linux-gnu/openblas-pthread

1
.rspec
View File

@ -1,3 +1,2 @@
--color
--require spec_helper
--format Fuubar

10
Aptfile
View File

@ -1,5 +1,5 @@
ffmpeg
libopenblas0-pthread
libpq-dev
libxdamage1
libxfixes3
libidn12
# for idn-ruby on heroku-24 stack
# use https://github.com/heroku/heroku-buildpack-activestorage-preview
# in place for ffmpeg and its dependent packages to reduce slag size

View File

@ -126,9 +126,6 @@ group :test do
# Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab
gem 'rspec-github', '~> 2.4', require: false
# RSpec progress bar formatter
gem 'fuubar', '~> 2.5'
# RSpec helpers for email specs
gem 'email_spec'
@ -154,6 +151,8 @@ group :test do
# Test harness fo rack components
gem 'rack-test', '~> 2.1'
gem 'shoulda-matchers'
# Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false
gem 'simplecov', '~> 0.22', require: false
gem 'simplecov-lcov', '~> 0.8', require: false

View File

@ -288,9 +288,6 @@ GEM
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
fuubar (2.5.1)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
google-protobuf (3.25.4)
@ -714,7 +711,7 @@ GEM
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.0)
rspec-core (3.13.1)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
@ -724,7 +721,7 @@ GEM
rspec-mocks (3.13.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.0.0)
rspec-rails (7.0.1)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
@ -793,6 +790,8 @@ GEM
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
semantic_range (3.0.0)
shoulda-matchers (6.3.1)
activesupport (>= 5.2.0)
sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3)
rack (~> 2.0)
@ -949,7 +948,6 @@ DEPENDENCIES
flatware-rspec
fog-core (<= 2.5.0)
fog-openstack (~> 1.0)
fuubar (~> 2.5)
haml-rails (~> 2.0)
haml_lint
hcaptcha (~> 7.1)
@ -1039,6 +1037,7 @@ DEPENDENCIES
sanitize (~> 6.0)
scenic (~> 1.7)
selenium-webdriver
shoulda-matchers
sidekiq (~> 6.5)
sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 5.0)

View File

@ -11,4 +11,4 @@ worker: bundle exec sidekiq
#
# and let the main app use the separate app:
#
# heroku config:set STREAMING_API_BASE_URL=wss://<streaming-app>.herokuapp.com -a <main-app>
# heroku config:set STREAMING_API_BASE_URL=wss://<streaming-app-random>.herokuapp.com -a <main-app>

View File

@ -90,9 +90,15 @@
}
},
"buildpacks": [
{
"url": "https://github.com/heroku/heroku-buildpack-activestorage-preview"
},
{
"url": "https://github.com/heroku/heroku-buildpack-apt"
},
{
"url": "heroku/nodejs"
},
{
"url": "heroku/ruby"
}
@ -100,5 +106,6 @@
"scripts": {
"postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed"
},
"addons": ["heroku-postgresql", "heroku-redis"]
"addons": ["heroku-postgresql", "heroku-redis"],
"stack": "heroku-24"
}

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
class Api::V2Alpha::Notifications::AccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }
before_action :require_user!
before_action :set_notifications!
after_action :insert_pagination_headers, only: :index
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
end
private
def load_accounts
@paginated_notifications.map(&:from_account)
end
def set_notifications!
@paginated_notifications = begin
current_account
.notifications
.without_suspended
.where(group_key: params[:notification_group_key])
.includes(from_account: [:account_stat, :user])
.paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end
end
def next_path
api_v2_alpha_notification_accounts_url pagination_params(max_id: pagination_max_id) if records_continue?
end
def prev_path
api_v2_alpha_notification_accounts_url pagination_params(min_id: pagination_since_id) unless @paginated_notifications.empty?
end
def pagination_collection
@paginated_notifications
end
def records_continue?
@paginated_notifications.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
end

View File

@ -46,7 +46,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
end
def show
@notification = current_account.notifications.without_suspended.find_by!(group_key: params[:id])
@notification = current_account.notifications.without_suspended.find_by!(group_key: params[:group_key])
presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
end
@ -57,7 +57,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
end
def dismiss
current_account.notifications.where(group_key: params[:id]).destroy_all
current_account.notifications.where(group_key: params[:group_key]).destroy_all
render_empty
end

View File

@ -106,11 +106,16 @@ module ApplicationHelper
end
def material_symbol(icon, attributes = {})
safe_join(
[
inline_svg_tag(
"400-24px/#{icon}.svg",
class: ['icon', "material-#{icon}"].concat(attributes[:class].to_s.split),
role: :img,
data: attributes[:data]
),
' ',
]
)
end

View File

@ -238,9 +238,7 @@ module LanguagesHelper
# Helper for self.sorted_locale_keys
private_class_method def self.locale_name_for_sorting(locale)
if locale.blank? || locale == 'und'
'000'
elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
if (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
ASCIIFolding.new.fold(supported_locale[1]).downcase
elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym])
ASCIIFolding.new.fold(regional_locale).downcase

View File

@ -1,12 +0,0 @@
import { saveSettings } from './settings';
export const LANGUAGE_USE = 'LANGUAGE_USE';
export const useLanguage = language => dispatch => {
dispatch({
type: LANGUAGE_USE,
language,
});
dispatch(saveSettings());
};

View File

@ -240,7 +240,6 @@ class LanguageDropdown extends PureComponent {
frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string),
intl: PropTypes.object.isRequired,
onChange: PropTypes.func,
onClose: PropTypes.func,
};
state = {
@ -257,14 +256,11 @@ class LanguageDropdown extends PureComponent {
};
handleClose = () => {
const { value, onClose } = this.props;
if (this.state.open && this.activeElement) {
this.activeElement.focus({ preventScroll: true });
}
this.setState({ open: false });
onClose(value);
};
handleChange = value => {

View File

@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { changeComposeLanguage } from 'mastodon/actions/compose';
import { useLanguage } from 'mastodon/actions/languages';
import LanguageDropdown from '../components/language_dropdown';
@ -28,11 +27,6 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeComposeLanguage(value));
},
onClose (value) {
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
dispatch(useLanguage(value));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(LanguageDropdown);

View File

@ -1,8 +1,8 @@
import { Map as ImmutableMap, fromJS } from 'immutable';
import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE, COLUMN_PARAMS_CHANGE } from '../actions/columns';
import { COMPOSE_LANGUAGE_CHANGE } from '../actions/compose';
import { EMOJI_USE } from '../actions/emojis';
import { LANGUAGE_USE } from '../actions/languages';
import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings';
@ -175,7 +175,7 @@ export default function settings(state = initialState, action) {
return changeColumnParams(state, action.uuid, action.path, action.value);
case EMOJI_USE:
return updateFrequentEmojis(state, action.emoji);
case LANGUAGE_USE:
case COMPOSE_LANGUAGE_CHANGE:
return updateFrequentLanguages(state, action.language);
case SETTING_SAVE:
return state.set('saved', true);

View File

@ -7580,7 +7580,7 @@ a.status-card {
}
}
.radio-button.checked::before {
.radio-button__input.checked::before {
position: absolute;
left: 2px;
top: 2px;

View File

@ -89,6 +89,7 @@ class Account < ApplicationRecord
include DomainMaterializable
include DomainNormalizable
include Paginable
include Reviewable
enum :protocol, { ostatus: 0, activitypub: 1 }
enum :suspension_origin, { local: 0, remote: 1 }, prefix: true
@ -145,6 +146,7 @@ class Account < ApplicationRecord
scope :with_username, ->(value) { where arel_table[:username].lower.eq(value.to_s.downcase) }
scope :with_domain, ->(value) { where arel_table[:domain].lower.eq(value&.to_s&.downcase) }
scope :without_memorial, -> { where(memorial: false) }
scope :duplicate_uris, -> { select(:uri, Arel.star.count).group(:uri).having(Arel.star.count.gt(1)) }
after_update_commit :trigger_update_webhooks
@ -421,22 +423,6 @@ class Account < ApplicationRecord
@synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
end
def requires_review?
reviewed_at.nil?
end
def reviewed?
reviewed_at.present?
end
def requested_review?
requested_review_at.present?
end
def requires_review_notification?
requires_review? && !requested_review?
end
class << self
def readonly_attributes
super - %w(statuses_count following_count followers_count)

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
module Reviewable
extend ActiveSupport::Concern
def requires_review?
reviewed_at.nil?
end
def reviewed?
reviewed_at.present?
end
def requested_review?
requested_review_at.present?
end
def requires_review_notification?
requires_review? && !requested_review?
end
end

View File

@ -21,6 +21,7 @@ class PreviewCardProvider < ApplicationRecord
include Paginable
include DomainNormalizable
include Attachmentable
include Reviewable
ICON_MIME_TYPES = %w(image/x-icon image/vnd.microsoft.icon image/png).freeze
LIMIT = 1.megabyte
@ -36,22 +37,6 @@ class PreviewCardProvider < ApplicationRecord
scope :reviewed, -> { where.not(reviewed_at: nil) }
scope :pending_review, -> { where(reviewed_at: nil) }
def requires_review?
reviewed_at.nil?
end
def reviewed?
reviewed_at.present?
end
def requested_review?
requested_review_at.present?
end
def requires_review_notification?
requires_review? && !requested_review?
end
def self.matching_domain(domain)
segments = domain.split('.')
where(domain: segments.map.with_index { |_, i| segments[i..].join('.') }).by_domain_length.first

View File

@ -21,6 +21,8 @@
class Tag < ApplicationRecord
include Paginable
include Reviewable
# rubocop:disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :statuses
has_and_belongs_to_many :accounts
@ -97,22 +99,6 @@ class Tag < ApplicationRecord
alias trendable? trendable
def requires_review?
reviewed_at.nil?
end
def reviewed?
reviewed_at.present?
end
def requested_review?
requested_review_at.present?
end
def requires_review_notification?
requires_review? && !requested_review?
end
def decaying?
max_score_at && max_score_at >= Trends.tags.options[:max_score_cooldown].ago && max_score_at < 1.day.ago
end

View File

@ -46,7 +46,7 @@
%th= t('imports.failures')
%tbody
- @recent_imports.each do |import|
%tr
%tr{ id: dom_id(import) }
%td= t("imports.types.#{import.type}")
%td
- if import.state_unconfirmed?

View File

@ -346,7 +346,7 @@ namespace :api, format: false do
end
namespace :v2_alpha do
resources :notifications, only: [:index, :show] do
resources :notifications, param: :group_key, only: [:index, :show] do
collection do
post :clear
get :unread_count
@ -355,6 +355,8 @@ namespace :api, format: false do
member do
post :dismiss
end
resources :accounts, only: [:index], module: :notifications
end
end

View File

@ -2,8 +2,8 @@
class RemoveUnneededIndexes < ActiveRecord::Migration[5.0]
def change
remove_index :notifications, name: 'index_notifications_on_account_id'
remove_index :settings, name: 'index_settings_on_target_type_and_target_id'
remove_index :statuses_tags, name: 'index_statuses_tags_on_tag_id'
remove_index :notifications, :account_id, name: 'index_notifications_on_account_id'
remove_index :settings, [:target_type, :target_id], name: 'index_settings_on_target_type_and_target_id'
remove_index :statuses_tags, :tag_id, name: 'index_statuses_tags_on_tag_id'
end
end

View File

@ -5,6 +5,6 @@ class AddIndexOnStreamEntries < ActiveRecord::Migration[5.2]
def change
add_index :stream_entries, [:account_id, :activity_type, :id], algorithm: :concurrently
remove_index :stream_entries, name: :index_stream_entries_on_account_id
remove_index :stream_entries, :account_id, name: :index_stream_entries_on_account_id
end
end

View File

@ -2,7 +2,7 @@
class RemoveDuplicateIndexesInLists < ActiveRecord::Migration[5.2]
def change
remove_index :list_accounts, name: 'index_list_accounts_on_account_id'
remove_index :list_accounts, name: 'index_list_accounts_on_list_id'
remove_index :list_accounts, :account_id, name: 'index_list_accounts_on_account_id'
remove_index :list_accounts, :list_id, name: 'index_list_accounts_on_list_id'
end
end

View File

@ -5,6 +5,6 @@ class MoreFasterIndexOnNotifications < ActiveRecord::Migration[5.2]
def change
add_index :notifications, [:account_id, :id], order: { id: :desc }, algorithm: :concurrently
remove_index :notifications, name: :index_notifications_on_id_and_account_id_and_activity_type
remove_index :notifications, [:id, :account_id, :activity_type], name: :index_notifications_on_id_and_account_id_and_activity_type
end
end

View File

@ -7,6 +7,6 @@ class AddIndexOnStatusesForApiV1AccountsAccountIdStatuses < ActiveRecord::Migrat
safety_assured do
add_index :statuses, [:account_id, :id, :visibility, :updated_at], order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20180106
end
remove_index :statuses, name: :index_statuses_on_account_id_id
remove_index :statuses, [:account_id, :id], name: :index_statuses_on_account_id_id
end
end

View File

@ -2,8 +2,8 @@
class RemoveUnusedIndexes < ActiveRecord::Migration[5.2]
def change
remove_index :statuses, name: 'index_statuses_on_conversation_id'
remove_index :users, name: 'index_users_on_filtered_languages'
remove_index :backups, name: 'index_backups_on_user_id'
remove_index :statuses, :conversation_id, name: 'index_statuses_on_conversation_id'
remove_index :users, :filtered_languages, name: 'index_users_on_filtered_languages'
remove_index :backups, :user_id, name: 'index_backups_on_user_id'
end
end

View File

@ -252,7 +252,7 @@ module Mastodon::CLI
domain configuration.
LONG_DESC
def fix_duplicates
Account.remote.select(:uri, 'count(*)').group(:uri).having('count(*) > 1').pluck(:uri).each do |uri|
Account.remote.duplicate_uris.pluck(:uri).each do |uri|
say("Duplicates found for #{uri}")
begin
ActivityPub::FetchRemoteAccountService.new.call(uri) unless dry_run?

View File

@ -25,10 +25,10 @@ RSpec.describe ActivityPub::CollectionsController do
context 'without signature' do
let(:remote_account) { nil }
it_behaves_like 'cacheable response'
it 'returns http success and correct media type and correct items' do
expect(response).to have_http_status(200)
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json'
expect(body_as_json[:orderedItems])
@ -64,10 +64,11 @@ RSpec.describe ActivityPub::CollectionsController do
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
context 'when getting a featured resource' do
it_behaves_like 'cacheable response'
it 'returns http success and correct media type and expected items' do
expect(response).to have_http_status(200)
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json'
expect(body_as_json[:orderedItems])

View File

@ -25,10 +25,11 @@ RSpec.describe ActivityPub::OutboxesController do
context 'with page not requested' do
let(:page) { nil }
it_behaves_like 'cacheable response'
it 'returns http success and correct media type and headers and items count' do
expect(response).to have_http_status(200)
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json'
expect(response.headers['Vary']).to be_nil
expect(body[:totalItems]).to eq 4
@ -59,10 +60,11 @@ RSpec.describe ActivityPub::OutboxesController do
context 'with page requested' do
let(:page) { 'true' }
it_behaves_like 'cacheable response'
it 'returns http success and correct media type and vary header and items' do
expect(response).to have_http_status(200)
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json'
expect(response.headers['Vary']).to include 'Signature'

View File

@ -68,10 +68,11 @@ RSpec.describe ActivityPub::RepliesController do
let(:parent_visibility) { :public }
let(:page_json) { body_as_json[:first] }
it_behaves_like 'cacheable response'
it 'returns http success and correct media type' do
expect(response).to have_http_status(200)
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.media_type).to eq 'application/activity+json'
end

View File

@ -40,15 +40,16 @@ RSpec.describe Admin::AccountsController do
expect(response)
.to have_http_status(200)
expect(assigns(:accounts))
.to have_attributes(
count: eq(1),
klass: be(Account)
)
expect(accounts_table_rows.size)
.to eq(1)
expect(AccountFilter)
.to have_received(:new)
.with(hash_including(params))
end
def accounts_table_rows
Nokogiri::Slop(response.body).css('table.accounts-table tr')
end
end
describe 'GET #show' do

View File

@ -13,7 +13,6 @@ RSpec.describe Admin::DomainAllowsController do
it 'assigns a new domain allow' do
get :new
expect(assigns(:domain_allow)).to be_instance_of(DomainAllow)
expect(response).to have_http_status(200)
end
end

View File

@ -13,7 +13,6 @@ RSpec.describe Admin::DomainBlocksController do
it 'assigns a new domain block' do
get :new
expect(assigns(:domain_block)).to be_instance_of(DomainBlock)
expect(response).to have_http_status(200)
end
end
@ -171,7 +170,6 @@ RSpec.describe Admin::DomainBlocksController do
it 'returns http success' do
get :edit, params: { id: domain_block.id }
expect(assigns(:domain_block)).to be_instance_of(DomainBlock)
expect(response).to have_http_status(200)
end
end

View File

@ -42,11 +42,8 @@ RSpec.describe Admin::ExportDomainBlocksController do
post :import, params: { admin_import: { data: fixture_file_upload('domain_blocks.csv') } }
end
it 'renders page with expected domain blocks' do
expect(assigns(:domain_blocks).map { |block| [block.domain, block.severity.to_sym] }).to contain_exactly(['bad.domain', :silence], ['worse.domain', :suspend], ['reject.media', :noop])
end
it 'returns http success' do
it 'renders page with expected domain blocks and returns http success' do
expect(mapped_batch_table_rows).to contain_exactly(['bad.domain', :silence], ['worse.domain', :suspend], ['reject.media', :noop])
expect(response).to have_http_status(200)
end
end
@ -56,14 +53,19 @@ RSpec.describe Admin::ExportDomainBlocksController do
post :import, params: { admin_import: { data: fixture_file_upload('domain_blocks_list.txt') } }
end
it 'renders page with expected domain blocks' do
expect(assigns(:domain_blocks).map { |block| [block.domain, block.severity.to_sym] }).to contain_exactly(['bad.domain', :suspend], ['worse.domain', :suspend], ['reject.media', :suspend])
end
it 'returns http success' do
it 'renders page with expected domain blocks and returns http success' do
expect(mapped_batch_table_rows).to contain_exactly(['bad.domain', :suspend], ['worse.domain', :suspend], ['reject.media', :suspend])
expect(response).to have_http_status(200)
end
end
def mapped_batch_table_rows
batch_table_rows.map { |row| [row.at_css('[id$=_domain]')['value'], row.at_css('[id$=_severity]')['value'].to_sym] }
end
def batch_table_rows
Nokogiri::Slop(response.body).css('body div.batch-table__row')
end
end
it 'displays error on no file selected' do

View File

@ -28,12 +28,15 @@ RSpec.describe Admin::InstancesController do
it 'renders instances' do
get :index, params: { page: 2 }
instances = assigns(:instances).to_a
expect(instances.size).to eq 1
expect(instances[0].domain).to eq 'less.popular'
expect(instance_directory_links.size).to eq(1)
expect(instance_directory_links.first.text.strip).to match('less.popular')
expect(response).to have_http_status(200)
end
def instance_directory_links
Nokogiri::Slop(response.body).css('div.directory__tag a')
end
end
describe 'GET #show' do

View File

@ -18,7 +18,8 @@ describe Admin::InvitesController do
it 'renders index page' do
expect(subject).to render_template :index
expect(assigns(:invites)).to include invite
expect(response.body)
.to include(invite.code)
end
end

View File

@ -13,39 +13,39 @@ describe Admin::ReportsController do
describe 'GET #index' do
it 'returns http success with no filters' do
specified = Fabricate(:report, action_taken_at: nil)
Fabricate(:report, action_taken_at: Time.now.utc)
specified = Fabricate(:report, action_taken_at: nil, comment: 'First report')
other = Fabricate(:report, action_taken_at: Time.now.utc, comment: 'Second report')
get :index
reports = assigns(:reports).to_a
expect(reports.size).to eq 1
expect(reports[0]).to eq specified
expect(response).to have_http_status(200)
expect(response.body)
.to include(specified.comment)
.and not_include(other.comment)
end
it 'returns http success with resolved filter' do
specified = Fabricate(:report, action_taken_at: Time.now.utc)
Fabricate(:report, action_taken_at: nil)
specified = Fabricate(:report, action_taken_at: Time.now.utc, comment: 'First report')
other = Fabricate(:report, action_taken_at: nil, comment: 'Second report')
get :index, params: { resolved: '1' }
reports = assigns(:reports).to_a
expect(reports.size).to eq 1
expect(reports[0]).to eq specified
expect(response).to have_http_status(200)
expect(response.body)
.to include(specified.comment)
.and not_include(other.comment)
end
end
describe 'GET #show' do
it 'renders report' do
report = Fabricate(:report)
report = Fabricate(:report, comment: 'A big problem')
get :show, params: { id: report }
expect(assigns(:report)).to eq report
expect(response).to have_http_status(200)
expect(response.body)
.to include(report.comment)
end
end

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Settings::AboutController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(:success)
end
end
describe 'PUT #update' do
it 'updates the settings' do
put :update, params: { form_admin_settings: { site_extended_description: 'new site description' } }
expect(response).to redirect_to(admin_settings_about_path)
end
end
end

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Settings::AppearanceController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(:success)
end
end
describe 'PUT #update' do
it 'updates the settings' do
put :update, params: { form_admin_settings: { custom_css: 'html { display: inline; }' } }
expect(response).to redirect_to(admin_settings_appearance_path)
end
end
end

View File

@ -10,14 +10,6 @@ RSpec.describe Admin::Settings::BrandingController do
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(200)
end
end
describe 'PUT #update' do
it 'cannot create a setting value for a non-admin key' do
expect(Setting.new_setting_key).to be_blank
@ -27,15 +19,6 @@ RSpec.describe Admin::Settings::BrandingController do
expect(response).to redirect_to(admin_settings_branding_path)
expect(Setting.new_setting_key).to be_nil
end
it 'creates a settings value that didnt exist before for eligible key' do
expect(Setting.site_short_description).to be_blank
patch :update, params: { form_admin_settings: { site_short_description: 'New key value' } }
expect(response).to redirect_to(admin_settings_branding_path)
expect(Setting.site_short_description).to eq 'New key value'
end
end
end
end

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Settings::ContentRetentionController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(:success)
end
end
describe 'PUT #update' do
it 'updates the settings' do
put :update, params: { form_admin_settings: { media_cache_retention_period: '2' } }
expect(response).to redirect_to(admin_settings_content_retention_path)
end
end
end

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Settings::DiscoveryController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(:success)
end
end
describe 'PUT #update' do
it 'updates the settings' do
put :update, params: { form_admin_settings: { trends: '1' } }
expect(response).to redirect_to(admin_settings_discovery_path)
end
end
end

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::Settings::RegistrationsController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(:success)
end
end
describe 'PUT #update' do
it 'updates the settings' do
put :update, params: { form_admin_settings: { registrations_mode: 'open' } }
expect(response).to redirect_to(admin_settings_registrations_path)
end
end
end

View File

@ -46,8 +46,9 @@ describe AuthorizeInteractionsController do
get :show, params: { acct: 'http://example.com' }
expect(response).to have_http_status(302)
expect(assigns(:resource)).to eq account
expect(response)
.to have_http_status(302)
.and redirect_to(web_url("@#{account.pretty_acct}"))
end
it 'sets resource from acct uri' do
@ -58,8 +59,9 @@ describe AuthorizeInteractionsController do
get :show, params: { acct: 'acct:found@hostname' }
expect(response).to have_http_status(302)
expect(assigns(:resource)).to eq account
expect(response)
.to have_http_status(302)
.and redirect_to(web_url("@#{account.pretty_acct}"))
end
end
end

View File

@ -7,7 +7,7 @@ describe AccountControllerConcern do
include AccountControllerConcern
def success
head 200
render plain: @account.username # rubocop:disable RSpec/InstanceVariable
end
end
@ -51,12 +51,13 @@ describe AccountControllerConcern do
context 'when account is not suspended' do
let(:account) { Fabricate(:account, username: 'username') }
it 'assigns @account, returns success, and sets link headers' do
it 'Prepares the account, returns success, and sets link headers' do
get 'success', params: { account_username: account.username }
expect(assigns(:account)).to eq account
expect(response).to have_http_status(200)
expect(response.headers['Link'].to_s).to eq(expected_link_headers)
expect(response.body)
.to include(account.username)
end
def expected_link_headers

View File

@ -21,9 +21,10 @@ RSpec.describe Settings::ImportsController do
it 'assigns the expected imports', :aggregate_failures do
expect(response).to have_http_status(200)
expect(assigns(:recent_imports)).to eq [import]
expect(assigns(:recent_imports)).to_not include(other_import)
expect(response.headers['Cache-Control']).to include('private, no-store')
expect(response.body)
.to include("bulk_import_#{import.id}")
.and not_include("bulk_import_#{other_import.id}")
end
end
@ -261,7 +262,8 @@ RSpec.describe Settings::ImportsController do
it 'does not creates an unconfirmed bulk_import', :aggregate_failures do
expect { subject }.to_not(change { user.account.bulk_imports.count })
expect(assigns(:import).errors).to_not be_empty
expect(response.body)
.to include('field_with_errors')
end
end

View File

@ -9,11 +9,16 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
it 'renders the new view' do
subject
expect(assigns(:confirmation)).to be_instance_of Form::TwoFactorConfirmation
expect(assigns(:provision_url)).to eq 'otpauth://totp/cb6e6126.ngrok.io:local-part%40domain?secret=thisisasecretforthespecofnewview&issuer=cb6e6126.ngrok.io'
expect(assigns(:qrcode)).to be_instance_of RQRCode::QRCode
expect(response).to have_http_status(200)
expect(response).to render_template(:new)
expect(response.body)
.to include(qr_code_markup)
end
def qr_code_markup
RQRCode::QRCode.new(
'otpauth://totp/cb6e6126.ngrok.io:local-part%40domain?secret=thisisasecretforthespecofnewview&issuer=cb6e6126.ngrok.io'
).as_svg(padding: 0, module_size: 4)
end
end
@ -61,10 +66,10 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
expect { post_create_with_options }
.to change { user.reload.otp_secret }.to 'thisisasecretforthespecofnewview'
expect(assigns(:recovery_codes)).to eq otp_backup_codes
expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled'
expect(response).to have_http_status(200)
expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
expect(response.body).to include(*otp_backup_codes)
end
end

View File

@ -15,10 +15,11 @@ describe Settings::TwoFactorAuthentication::RecoveryCodesController do
sign_in user, scope: :user
post :create, session: { challenge_passed_at: Time.now.utc }
expect(assigns(:recovery_codes)).to eq otp_backup_codes
expect(flash[:notice]).to eq 'Recovery codes successfully regenerated'
expect(response).to have_http_status(200)
expect(response).to render_template(:index)
expect(response.body)
.to include(*otp_backup_codes)
end
it 'redirects when not signed in' do

View File

@ -72,13 +72,12 @@ describe StatusesController do
context 'with JSON' do
let(:format) { 'json' }
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'renders ActivityPub Note object successfully', :aggregate_failures do
expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.headers).to include(
'Vary' => 'Accept, Accept-Language, Cookie',
'Content-Type' => include('application/activity+json'),
'Link' => satisfy { |header| header.to_s.include?('activity+json') }
)
@ -380,13 +379,11 @@ describe StatusesController do
context 'with JSON' do
let(:format) { 'json' }
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'renders ActivityPub Note object successfully', :aggregate_failures do
expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.headers).to include(
'Vary' => 'Accept, Accept-Language, Cookie',
'Content-Type' => include('application/activity+json'),
'Link' => satisfy { |header| header.to_s.include?('activity+json') }
)

View File

@ -1,45 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe TagsController do
render_views
describe 'GET #show' do
let(:format) { 'html' }
let(:tag) { Fabricate(:tag, name: 'test') }
let(:tag_name) { tag&.name }
before do
get :show, params: { id: tag_name, format: format }
end
context 'when tag exists' do
context 'when requested as HTML' do
it 'returns http success' do
expect(response).to have_http_status(200)
end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
end
context 'when requested as JSON' do
let(:format) { 'json' }
it 'returns http success' do
expect(response).to have_http_status(200)
end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
end
end
context 'when tag does not exist' do
let(:tag_name) { 'hoge' }
it 'returns http not found' do
expect(response).to have_http_status(404)
end
end
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
Fabricator(:generated_annual_report) do
account { Fabricate.build(:account) }
data { { test: :data } }
schema_version { AnnualReport::SCHEMA }
year { sequence(:year) { |i| 2000 + i } }
end

View File

@ -23,6 +23,19 @@ describe StatusesHelper do
end
end
describe '#media_summary' do
it 'describes the media on a status' do
status = Fabricate :status
Fabricate :media_attachment, status: status, type: :video
Fabricate :media_attachment, status: status, type: :audio
Fabricate :media_attachment, status: status, type: :image
result = helper.media_summary(status)
expect(result).to eq('Attached: 1 image · 1 video · 1 audio')
end
end
describe 'fa_visibility_icon' do
context 'with a status that is public' do
let(:status) { Status.new(visibility: 'public') }

View File

@ -613,6 +613,25 @@ describe Mastodon::CLI::Accounts do
end
end
describe '#fix_duplicates' do
let(:action) { :fix_duplicates }
let(:service_double) { instance_double(ActivityPub::FetchRemoteAccountService, call: nil) }
let(:uri) { 'https://host.example/same/value' }
context 'when there are duplicate URI accounts' do
before do
Fabricate.times(2, :account, domain: 'host.example', uri: uri)
allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(service_double)
end
it 'finds the duplicates and calls fetch remote account service' do
expect { subject }
.to output_results('Duplicates found')
expect(service_double).to have_received(:call).with(uri)
end
end
end
describe '#backup' do
let(:action) { :backup }

View File

@ -3,6 +3,8 @@
require 'rails_helper'
RSpec.describe Account do
include_examples 'Reviewable'
context 'with an account record' do
subject { Fabricate(:account) }
@ -722,11 +724,7 @@ RSpec.describe Account do
end
describe 'validations' do
it 'is invalid without a username' do
account = Fabricate.build(:account, username: nil)
account.valid?
expect(account).to model_have_error_on_field(:username)
end
it { is_expected.to validate_presence_of(:username) }
it 'squishes the username before validation' do
account = Fabricate(:account, domain: nil, username: " \u3000bob \t \u00a0 \n ")

View File

@ -3,6 +3,8 @@
require 'rails_helper'
describe PreviewCardProvider do
include_examples 'Reviewable'
describe 'scopes' do
let(:trendable_and_reviewed) { Fabricate(:preview_card_provider, trendable: true, reviewed_at: 5.days.ago) }
let(:not_trendable_and_not_reviewed) { Fabricate(:preview_card_provider, trendable: false, reviewed_at: nil) }

View File

@ -3,6 +3,8 @@
require 'rails_helper'
RSpec.describe Tag do
include_examples 'Reviewable'
describe 'validations' do
it 'invalid with #' do
expect(described_class.new(name: '#hello_world')).to_not be_valid

View File

@ -38,28 +38,28 @@ RSpec.describe Webhook do
describe '#rotate_secret!' do
it 'changes the secret' do
previous_value = webhook.secret
webhook.rotate_secret!
expect(webhook.secret).to_not be_blank
expect(webhook.secret).to_not eq previous_value
expect { webhook.rotate_secret! }
.to change(webhook, :secret)
expect(webhook.secret)
.to_not be_blank
end
end
describe '#enable!' do
before do
webhook.disable!
end
let(:webhook) { Fabricate(:webhook, enabled: false) }
it 'enables the webhook' do
webhook.enable!
expect(webhook.enabled?).to be true
expect { webhook.enable! }
.to change(webhook, :enabled?).to(true)
end
end
describe '#disable!' do
let(:webhook) { Fabricate(:webhook, enabled: true) }
it 'disables the webhook' do
webhook.disable!
expect(webhook.enabled?).to be false
expect { webhook.disable! }
.to change(webhook, :enabled?).to(false)
end
end
end

View File

@ -55,6 +55,8 @@ Sidekiq.logger = nil
DatabaseCleaner.strategy = [:deletion]
Chewy.settings[:enabled] = false
Devise::Test::ControllerHelpers.module_eval do
alias_method :original_sign_in, :sign_in
@ -112,6 +114,7 @@ RSpec.configure do |config|
config.include ThreadingHelpers
config.include SignedRequestHelpers, type: :request
config.include CommandLineHelpers, type: :cli
config.include SystemHelpers, type: :system
config.around(:each, use_transactional_tests: false) do |example|
self.use_transactional_tests = false
@ -128,6 +131,12 @@ RSpec.configure do |config|
example.run
end
config.around(:each, type: :search) do |example|
Chewy.settings[:enabled] = true
example.run
Chewy.settings[:enabled] = false
end
config.before :each, type: :cli do
stub_reset_connection_pools
end
@ -138,10 +147,19 @@ RSpec.configure do |config|
config.before do |example|
unless example.metadata[:attachment_processing]
allow_any_instance_of(Paperclip::Attachment).to receive(:post_process).and_return(true) # rubocop:disable RSpec/AnyInstance
# rubocop:disable RSpec/AnyInstance
allow_any_instance_of(Paperclip::Attachment).to receive(:post_process).and_return(true)
allow_any_instance_of(Paperclip::MediaTypeSpoofDetector).to receive(:spoofed?).and_return(false)
# rubocop:enable RSpec/AnyInstance
end
end
config.before :each, type: :request do
# Use https and configured hostname in request spec requests
integration_session.https!
host! Rails.configuration.x.local_domain
end
config.after do
Rails.cache.clear
redis.del(redis.keys)

View File

@ -14,7 +14,7 @@ describe 'The account show page' do
expect(head_meta_content('og:title')).to match alice.display_name
expect(head_meta_content('og:type')).to eq 'profile'
expect(head_meta_content('og:image')).to match '.+'
expect(head_meta_content('og:url')).to match 'http://.+'
expect(head_meta_content('og:url')).to eq short_account_url(username: alice.username)
end
def head_link_icons

View File

@ -130,6 +130,7 @@ describe 'Accounts show response' do
it 'returns a JSON version of the account', :aggregate_failures do
expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
.and have_attributes(
media_type: eq('application/activity+json')
)
@ -137,8 +138,6 @@ describe 'Accounts show response' do
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 }
@ -179,6 +178,7 @@ describe 'Accounts show response' do
it 'returns a JSON version of the account', :aggregate_failures do
expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
.and have_attributes(
media_type: eq('application/activity+json')
)
@ -186,8 +186,6 @@ describe 'Accounts show response' do
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 }
@ -215,10 +213,10 @@ describe 'Accounts show response' do
get short_account_path(username: account.username, format: format)
end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses', :aggregate_failures do
expect(response).to have_http_status(200)
expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.body).to include(status_tag_for(status_media))
expect(response.body).to include(status_tag_for(status_self_reply))
expect(response.body).to include(status_tag_for(status))
@ -234,10 +232,11 @@ describe 'Accounts show response' do
get short_account_with_replies_path(username: account.username, format: format)
end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with replies', :aggregate_failures do
expect(response).to have_http_status(200)
expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.body).to include(status_tag_for(status_media))
expect(response.body).to include(status_tag_for(status_reply))
expect(response.body).to include(status_tag_for(status_self_reply))
@ -253,10 +252,10 @@ describe 'Accounts show response' do
get short_account_media_path(username: account.username, format: format)
end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with media', :aggregate_failures do
expect(response).to have_http_status(200)
expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.body).to include(status_tag_for(status_media))
expect(response.body).to_not include(status_tag_for(status_direct))
expect(response.body).to_not include(status_tag_for(status_private))
@ -277,10 +276,11 @@ describe 'Accounts show response' do
get short_account_tag_path(username: account.username, tag: tag, format: format)
end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with a tag', :aggregate_failures do
expect(response).to have_http_status(200)
expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.body).to include(status_tag_for(status_tag))
expect(response.body).to_not include(status_tag_for(status_direct))
expect(response.body).to_not include(status_tag_for(status_media))

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'API V1 Annual Reports' do
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
describe 'GET /api/v1/annual_reports' do
context 'when not authorized' do
it 'returns http unauthorized' do
get api_v1_annual_reports_path
expect(response)
.to have_http_status(401)
end
end
context 'with wrong scope' do
before do
get api_v1_annual_reports_path, headers: headers
end
it_behaves_like 'forbidden for wrong scope', 'write write:accounts'
end
context 'with correct scope' do
let(:scopes) { 'read:accounts' }
it 'returns http success' do
get api_v1_annual_reports_path, headers: headers
expect(response)
.to have_http_status(200)
expect(body_as_json)
.to be_present
end
end
end
describe 'POST /api/v1/annual_reports/:id/read' do
context 'with correct scope' do
let(:scopes) { 'write:accounts' }
it 'returns success and marks the report as read' do
annual_report = Fabricate :generated_annual_report, account: user.account
expect { post read_api_v1_annual_report_path(id: annual_report.year), headers: headers }
.to change { annual_report.reload.viewed? }.to(true)
expect(response)
.to have_http_status(200)
end
end
end
end

View File

@ -10,12 +10,11 @@ describe 'API V1 Streaming' do
Rails.configuration.x.streaming_api_base_url = before
end
let(:headers) { { 'Host' => Rails.configuration.x.web_domain } }
context 'with streaming api on same host' do
describe 'GET /api/v1/streaming' do
it 'raises ActiveRecord::RecordNotFound' do
get '/api/v1/streaming', headers: headers
integration_session.https!(false)
get '/api/v1/streaming'
expect(response).to have_http_status(404)
end

View File

@ -8,15 +8,6 @@ RSpec.describe 'Tag' do
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
shared_examples 'a successful request to the tag timeline' do
it 'returns the expected statuses', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(body_as_json.pluck(:id)).to match_array(expected_statuses.map { |status| status.id.to_s })
end
end
describe 'GET /api/v1/timelines/tag/:hashtag' do
subject do
get "/api/v1/timelines/tag/#{hashtag}", headers: headers, params: params
@ -26,8 +17,20 @@ RSpec.describe 'Tag' do
Setting.timeline_preview = true
end
shared_examples 'a successful request to the tag timeline' do
it 'returns the expected statuses', :aggregate_failures do
subject
expect(response)
.to have_http_status(200)
expect(body_as_json.pluck(:id))
.to match_array(expected_statuses.map { |status| status.id.to_s })
.and not_include(private_status.id)
end
end
let(:account) { Fabricate(:account) }
let!(:private_status) { PostStatusService.new.call(account, visibility: :private, text: '#life could be a dream') } # rubocop:disable RSpec/LetSetup
let!(:private_status) { PostStatusService.new.call(account, visibility: :private, text: '#life could be a dream') }
let!(:life_status) { PostStatusService.new.call(account, text: 'tell me what is my #life without your #love') }
let!(:war_status) { PostStatusService.new.call(user.account, text: '#war, war never changes') }
let!(:love_status) { PostStatusService.new.call(account, text: 'what is #love?') }

View File

@ -0,0 +1,80 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Accounts in grouped notifications' 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_alpha/notifications/:group_key/accounts', :inline_jobs do
subject do
get "/api/v2_alpha/notifications/#{user.account.notifications.first.group_key}/accounts", headers: headers, params: params
end
let(:params) { {} }
before do
first_status = PostStatusService.new.call(user.account, text: 'Test')
FavouriteService.new.call(Fabricate(:account), first_status)
FavouriteService.new.call(Fabricate(:account), first_status)
ReblogService.new.call(Fabricate(:account), first_status)
FollowService.new.call(Fabricate(:account), user.account)
FavouriteService.new.call(Fabricate(:account), first_status)
end
it_behaves_like 'forbidden for wrong scope', 'write write:notifications'
it 'returns a list of accounts' do
subject
expect(response).to have_http_status(200)
# The group we are interested in is only favorites
notifications = user.account.notifications.where(type: 'favourite').reorder(id: :desc)
expect(body_as_json).to match(
[
a_hash_including(
id: notifications.first.from_account_id.to_s
),
a_hash_including(
id: notifications.second.from_account_id.to_s
),
a_hash_including(
id: notifications.third.from_account_id.to_s
),
]
)
end
context 'with limit param' do
let(:params) { { limit: 2 } }
it 'returns the requested number of accounts, with pagination headers' do
subject
expect(response).to have_http_status(200)
# The group we are interested in is only favorites
notifications = user.account.notifications.where(type: 'favourite').reorder(id: :desc)
expect(body_as_json).to match(
[
a_hash_including(
id: notifications.first.from_account_id.to_s
),
a_hash_including(
id: notifications.second.from_account_id.to_s
),
]
)
expect(response)
.to include_pagination_headers(
prev: api_v2_alpha_notification_accounts_url(limit: params[:limit], min_id: notifications.first.id),
next: api_v2_alpha_notification_accounts_url(limit: params[:limit], max_id: notifications.second.id)
)
end
end
end
end

View File

@ -9,11 +9,10 @@ describe 'Custom stylesheets' do
it 'returns http success' do
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
.and have_attributes(
content_type: match('text/css')
)
end
it_behaves_like 'cacheable response'
end
end

View File

@ -17,6 +17,7 @@ RSpec.describe 'Instance actor endpoint' do
it 'returns http success with correct media type and body' do
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
expect(response.content_type)
.to start_with('application/activity+json')
expect(body_as_json)
@ -32,8 +33,6 @@ RSpec.describe 'Instance actor endpoint' do
url: about_more_url(instance_actor: true)
)
end
it_behaves_like 'cacheable response'
end
context 'with limited federation mode disabled' do

View File

@ -13,7 +13,7 @@ describe 'Link headers' do
it 'contains webfinger url in link header' do
link_header = link_header_with_type('application/jrd+json')
expect(link_header.href).to eq 'http://www.example.com/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io'
expect(link_header.href).to eq 'https://cb6e6126.ngrok.io/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io'
expect(link_header.attr_pairs.first).to eq %w(rel lrdd)
end

View File

@ -9,6 +9,7 @@ describe 'Manifest' do
it 'returns http success' do
expect(response)
.to have_http_status(200)
.and have_cacheable_headers
.and have_attributes(
content_type: match('application/json')
)
@ -18,7 +19,5 @@ describe 'Manifest' do
name: 'Mastodon Glitch Edition'
)
end
it_behaves_like 'cacheable response'
end
end

View File

@ -4,12 +4,7 @@ require 'rails_helper'
describe 'Media Proxy' do
describe 'GET /media_proxy/:id' do
before do
integration_session.https! # TODO: Move to global rails_helper for all request specs?
host! Rails.configuration.x.local_domain # TODO: Move to global rails_helper for all request specs?
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
end
before { stub_attachment_request }
context 'when attached to a status' do
let(:status) { Fabricate(:status) }
@ -63,5 +58,15 @@ describe 'Media Proxy' do
.to have_http_status(404)
end
end
def stub_attachment_request
stub_request(
:get,
'http://example.com/attachment.png'
)
.to_return(
request_fixture('avatar.txt')
)
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Tags' do
describe 'GET /tags/:id' do
context 'when tag exists' do
let(:tag) { Fabricate :tag }
context 'with HTML format' do
# TODO: Update the have_cacheable_headers matcher to operate on capybara sessions
# Remove this example, rely on system spec (which should use matcher)
before { get tag_path(tag) }
it 'returns http success' do
expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
end
end
context 'with JSON format' do
before { get tag_path(tag, format: :json) }
it 'returns http success' do
expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.content_type)
.to start_with('application/activity+json')
end
end
context 'with RSS format' do
before { get tag_path(tag, format: :rss) }
it 'returns http success' do
expect(response)
.to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
expect(response.content_type)
.to start_with('application/rss+xml')
end
end
end
context 'when tag does not exist' do
before { get tag_path('missing') }
it 'returns http not found' do
expect(response)
.to have_http_status(404)
end
end
end
end

View File

@ -8,12 +8,6 @@ RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
config.around(:example, :without_verify_partial_doubles) do |example|
mocks.verify_partial_doubles = false
example.call
mocks.verify_partial_doubles = true
end
end
config.before :suite do

View File

@ -1,14 +0,0 @@
# frozen_string_literal: true
shared_examples 'cacheable response' do |expects_vary: false|
it 'sets correct cache and vary headers and does not set cookies or session', :aggregate_failures do
expect(response.cookies).to be_empty
expect(response.headers['Set-Cookies']).to be_nil
expect(session).to be_empty
expect(response.headers['Vary']).to include(expects_vary) if expects_vary
expect(response.headers['Cache-Control']).to include('public')
end
end

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
shared_examples 'Reviewable' do
subject { described_class.new(reviewed_at: reviewed_at, requested_review_at: requested_review_at) }
let(:reviewed_at) { nil }
let(:requested_review_at) { nil }
describe '#requires_review?' do
it { is_expected.to be_requires_review }
context 'when reviewed_at is not null' do
let(:reviewed_at) { 5.days.ago }
it { is_expected.to_not be_requires_review }
end
end
describe '#reviewed?' do
it { is_expected.to_not be_reviewed }
context 'when reviewed_at is not null' do
let(:reviewed_at) { 5.days.ago }
it { is_expected.to be_reviewed }
end
end
describe '#requested_review?' do
it { is_expected.to_not be_requested_review }
context 'when requested_reviewed_at is not null' do
let(:requested_review_at) { 5.days.ago }
it { is_expected.to be_requested_review }
end
end
describe '#requires_review_notification?' do
it { is_expected.to be_requires_review_notification }
context 'when reviewed_at is not null' do
let(:reviewed_at) { 5.days.ago }
it { is_expected.to_not be_requires_review_notification }
end
context 'when requested_reviewed_at is not null' do
let(:requested_review_at) { 5.days.ago }
it { is_expected.to_not be_requires_review_notification }
end
end
end

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
RSpec::Matchers.define :have_cacheable_headers do
match do |response|
@response = response
@errors = [].tap do |errors|
errors << check_cookies
errors << check_cookie_headers
errors << check_session
errors << check_cache_control
errors << check_vary if @expected_vary.present?
end
@errors.compact.empty?
end
chain :with_vary do |string|
@expected_vary = string
end
failure_message do
<<~ERROR
Expected that the response would be cacheable but it was not:
- #{@errors.compact.join("\n - ")}
ERROR
end
def check_vary
puts @expected_vary
pp @response.headers
"Response `Vary` header does not contain `#{@expected_vary}`" unless @response.headers['Vary'].include?(@expected_vary)
end
def check_cookies
'Reponse cookies are present' unless @response.cookies.empty?
end
def check_cookie_headers
'Response `Set-Cookies` headers are present' if @response.headers['Set-Cookies'].present?
end
def check_session
'The session is not empty' unless session.empty?
end
def check_cache_control
'The `Cache-Control` header does not contain `public`' unless @response.headers['Cache-Control'].include?('public')
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module SystemHelpers
def admin_user
Fabricate(:user, role: UserRole.find_by(name: 'Admin'))
end
def submit_button
I18n.t('generic.save_changes')
end
def success_message
I18n.t('generic.changes_saved_msg')
end
def form_label(key)
I18n.t key, scope: 'simple_form.labels'
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'Admin::Settings::About' do
it 'Saves changes to about settings' do
sign_in admin_user
visit admin_settings_about_path
fill_in extended_description_field,
with: 'new site description'
click_on submit_button
expect(page)
.to have_content(success_message)
end
def extended_description_field
form_label 'form_admin_settings.site_extended_description'
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'Admin::Settings::Appearance' do
it 'Saves changes to appearance settings' do
sign_in admin_user
visit admin_settings_appearance_path
fill_in custom_css_field,
with: 'html { display: inline; }'
click_on submit_button
expect(page)
.to have_content(success_message)
end
def custom_css_field
form_label 'form_admin_settings.custom_css'
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'Admin::Settings::Branding' do
it 'Saves changes to branding settings' do
sign_in admin_user
visit admin_settings_branding_path
fill_in short_description_field,
with: 'new key value'
fill_in site_contact_email_field,
with: User.last.email
fill_in site_contact_username_field,
with: Account.last.username
expect { click_on submit_button }
.to change(Setting, :site_short_description).to('new key value')
expect(page)
.to have_content(success_message)
end
def short_description_field
form_label 'form_admin_settings.site_short_description'
end
def site_contact_email_field
form_label 'form_admin_settings.site_contact_email'
end
def site_contact_username_field
form_label 'form_admin_settings.site_contact_username'
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'Admin::Settings::ContentRetention' do
it 'Saves changes to content retention settings' do
sign_in admin_user
visit admin_settings_content_retention_path
fill_in media_cache_retention_period_field,
with: '2'
click_on submit_button
expect(page)
.to have_content(success_message)
end
def media_cache_retention_period_field
form_label 'form_admin_settings.media_cache_retention_period'
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'Admin::Settings::Discovery' do
it 'Saves changes to discovery settings' do
sign_in admin_user
visit admin_settings_discovery_path
check trends_box
click_on submit_button
expect(page)
.to have_content(success_message)
end
def trends_box
form_label 'form_admin_settings.trends'
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'Admin::Settings::Registrations' do
it 'Saves changes to registrations settings' do
sign_in admin_user
visit admin_settings_registrations_path
select open_mode_option,
from: registrations_mode_field
click_on submit_button
expect(page)
.to have_content(success_message)
end
def open_mode_option
I18n.t('admin.settings.registrations_mode.modes.open')
end
def registrations_mode_field
form_label 'form_admin_settings.registrations_mode'
end
end

16
spec/system/tags_spec.rb Normal file
View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Tags' do
describe 'Viewing a tag' do
let(:tag) { Fabricate(:tag, name: 'test') }
it 'visits the tag page and renders the web app' do
visit tag_path(tag)
expect(page)
.to have_css('noscript', text: /Mastodon/)
end
end
end

View File

@ -2,14 +2,13 @@
require 'rails_helper'
describe 'statuses/show.html.haml', :without_verify_partial_doubles do
describe 'statuses/show.html.haml' do
let(:alice) { Fabricate(:account, username: 'alice', display_name: 'Alice') }
let(:status) { Fabricate(:status, account: alice, text: 'Hello World') }
before do
allow(view).to receive_messages(api_oembed_url: '', site_title: 'example site', site_hostname: 'example.com', full_asset_url: '//asset.host/image.svg', current_flavour: 'glitch', current_account: nil, single_user_mode?: false)
allow(view).to receive(:local_time)
allow(view).to receive(:local_time_ago)
view.extend view_helpers
assign(:instance_presenter, InstancePresenter.new)
Fabricate(:media_attachment, account: alice, status: status, type: :video)
@ -40,4 +39,19 @@ describe 'statuses/show.html.haml', :without_verify_partial_doubles do
def header_tags
view.content_for(:header_tags)
end
def view_helpers
Module.new do
def api_oembed_url(_) = ''
def show_landing_strip? = true
def site_title = 'example site'
def site_hostname = 'example.com'
def full_asset_url(_) = '//asset.host/image.svg'
def current_account = nil
def current_flavour = 'glitch'
def single_user_mode? = false
def local_time = nil
def local_time_ago = nil
end
end
end