Merge pull request #2834 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to c9ea91f868
This commit is contained in:
Claire 2024-09-03 23:26:20 +02:00 committed by GitHub
commit 664dfa69b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 854 additions and 502 deletions

3
.gitignore vendored
View file

@ -71,3 +71,6 @@ docker-compose.override.yml
# Ignore dotenv .local files # Ignore dotenv .local files
.env*.local .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 --color
--require spec_helper --require spec_helper
--format Fuubar

10
Aptfile
View file

@ -1,5 +1,5 @@
ffmpeg libidn12
libopenblas0-pthread # for idn-ruby on heroku-24 stack
libpq-dev
libxdamage1 # use https://github.com/heroku/heroku-buildpack-activestorage-preview
libxfixes3 # 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 # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab
gem 'rspec-github', '~> 2.4', require: false gem 'rspec-github', '~> 2.4', require: false
# RSpec progress bar formatter
gem 'fuubar', '~> 2.5'
# RSpec helpers for email specs # RSpec helpers for email specs
gem 'email_spec' gem 'email_spec'
@ -154,6 +151,8 @@ group :test do
# Test harness fo rack components # Test harness fo rack components
gem 'rack-test', '~> 2.1' gem 'rack-test', '~> 2.1'
gem 'shoulda-matchers'
# Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false
gem 'simplecov', '~> 0.22', require: false gem 'simplecov', '~> 0.22', require: false
gem 'simplecov-lcov', '~> 0.8', require: false gem 'simplecov-lcov', '~> 0.8', require: false

View file

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

View file

@ -11,4 +11,4 @@ worker: bundle exec sidekiq
# #
# and let the main app use the separate app: # 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": [ "buildpacks": [
{
"url": "https://github.com/heroku/heroku-buildpack-activestorage-preview"
},
{ {
"url": "https://github.com/heroku/heroku-buildpack-apt" "url": "https://github.com/heroku/heroku-buildpack-apt"
}, },
{
"url": "heroku/nodejs"
},
{ {
"url": "heroku/ruby" "url": "heroku/ruby"
} }
@ -100,5 +106,6 @@
"scripts": { "scripts": {
"postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed" "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 end
def show 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])) presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
render json: presenter, serializer: REST::DedupNotificationGroupSerializer render json: presenter, serializer: REST::DedupNotificationGroupSerializer
end end
@ -57,7 +57,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
end end
def dismiss 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 render_empty
end end

View file

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

View file

@ -238,9 +238,7 @@ module LanguagesHelper
# Helper for self.sorted_locale_keys # Helper for self.sorted_locale_keys
private_class_method def self.locale_name_for_sorting(locale) private_class_method def self.locale_name_for_sorting(locale)
if locale.blank? || locale == 'und' if (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
'000'
elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
ASCIIFolding.new.fold(supported_locale[1]).downcase ASCIIFolding.new.fold(supported_locale[1]).downcase
elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym]) elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym])
ASCIIFolding.new.fold(regional_locale).downcase 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), frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string),
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
onChange: PropTypes.func, onChange: PropTypes.func,
onClose: PropTypes.func,
}; };
state = { state = {
@ -257,14 +256,11 @@ class LanguageDropdown extends PureComponent {
}; };
handleClose = () => { handleClose = () => {
const { value, onClose } = this.props;
if (this.state.open && this.activeElement) { if (this.state.open && this.activeElement) {
this.activeElement.focus({ preventScroll: true }); this.activeElement.focus({ preventScroll: true });
} }
this.setState({ open: false }); this.setState({ open: false });
onClose(value);
}; };
handleChange = value => { handleChange = value => {

View file

@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { changeComposeLanguage } from 'flavours/glitch/actions/compose'; import { changeComposeLanguage } from 'flavours/glitch/actions/compose';
import { useLanguage } from 'flavours/glitch/actions/languages';
import LanguageDropdown from '../components/language_dropdown'; import LanguageDropdown from '../components/language_dropdown';
@ -28,11 +27,6 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeComposeLanguage(value)); 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); export default connect(mapStateToProps, mapDispatchToProps)(LanguageDropdown);

View file

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

View file

@ -8112,7 +8112,7 @@ img.modal-warning {
} }
} }
.radio-button.checked::before { .radio-button__input.checked::before {
position: absolute; position: absolute;
left: 2px; left: 2px;
top: 2px; top: 2px;

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

View file

@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { changeComposeLanguage } from 'mastodon/actions/compose'; import { changeComposeLanguage } from 'mastodon/actions/compose';
import { useLanguage } from 'mastodon/actions/languages';
import LanguageDropdown from '../components/language_dropdown'; import LanguageDropdown from '../components/language_dropdown';
@ -28,11 +27,6 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeComposeLanguage(value)); 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); export default connect(mapStateToProps, mapDispatchToProps)(LanguageDropdown);

View file

@ -1,8 +1,8 @@
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap, fromJS } from 'immutable';
import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE, COLUMN_PARAMS_CHANGE } from '../actions/columns'; 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 { EMOJI_USE } from '../actions/emojis';
import { LANGUAGE_USE } from '../actions/languages';
import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists'; import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications'; import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings'; 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); return changeColumnParams(state, action.uuid, action.path, action.value);
case EMOJI_USE: case EMOJI_USE:
return updateFrequentEmojis(state, action.emoji); return updateFrequentEmojis(state, action.emoji);
case LANGUAGE_USE: case COMPOSE_LANGUAGE_CHANGE:
return updateFrequentLanguages(state, action.language); return updateFrequentLanguages(state, action.language);
case SETTING_SAVE: case SETTING_SAVE:
return state.set('saved', true); 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; position: absolute;
left: 2px; left: 2px;
top: 2px; top: 2px;

View file

@ -89,6 +89,7 @@ class Account < ApplicationRecord
include DomainMaterializable include DomainMaterializable
include DomainNormalizable include DomainNormalizable
include Paginable include Paginable
include Reviewable
enum :protocol, { ostatus: 0, activitypub: 1 } enum :protocol, { ostatus: 0, activitypub: 1 }
enum :suspension_origin, { local: 0, remote: 1 }, prefix: true 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_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 :with_domain, ->(value) { where arel_table[:domain].lower.eq(value&.to_s&.downcase) }
scope :without_memorial, -> { where(memorial: false) } 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 after_update_commit :trigger_update_webhooks
@ -421,22 +423,6 @@ class Account < ApplicationRecord
@synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/" @synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
end 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 class << self
def readonly_attributes def readonly_attributes
super - %w(statuses_count following_count followers_count) 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 Paginable
include DomainNormalizable include DomainNormalizable
include Attachmentable include Attachmentable
include Reviewable
ICON_MIME_TYPES = %w(image/x-icon image/vnd.microsoft.icon image/png).freeze ICON_MIME_TYPES = %w(image/x-icon image/vnd.microsoft.icon image/png).freeze
LIMIT = 1.megabyte LIMIT = 1.megabyte
@ -36,22 +37,6 @@ class PreviewCardProvider < ApplicationRecord
scope :reviewed, -> { where.not(reviewed_at: nil) } scope :reviewed, -> { where.not(reviewed_at: nil) }
scope :pending_review, -> { where(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) def self.matching_domain(domain)
segments = domain.split('.') segments = domain.split('.')
where(domain: segments.map.with_index { |_, i| segments[i..].join('.') }).by_domain_length.first where(domain: segments.map.with_index { |_, i| segments[i..].join('.') }).by_domain_length.first

View file

@ -21,6 +21,8 @@
class Tag < ApplicationRecord class Tag < ApplicationRecord
include Paginable include Paginable
include Reviewable
# rubocop:disable Rails/HasAndBelongsToMany # rubocop:disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :statuses has_and_belongs_to_many :statuses
has_and_belongs_to_many :accounts has_and_belongs_to_many :accounts
@ -97,22 +99,6 @@ class Tag < ApplicationRecord
alias trendable? trendable 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? def decaying?
max_score_at && max_score_at >= Trends.tags.options[:max_score_cooldown].ago && max_score_at < 1.day.ago max_score_at && max_score_at >= Trends.tags.options[:max_score_cooldown].ago && max_score_at < 1.day.ago
end end

View file

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

View file

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

View file

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

View file

@ -5,6 +5,6 @@ class AddIndexOnStreamEntries < ActiveRecord::Migration[5.2]
def change def change
add_index :stream_entries, [:account_id, :activity_type, :id], algorithm: :concurrently 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
end end

View file

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

View file

@ -5,6 +5,6 @@ class MoreFasterIndexOnNotifications < ActiveRecord::Migration[5.2]
def change def change
add_index :notifications, [:account_id, :id], order: { id: :desc }, algorithm: :concurrently 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
end end

View file

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

View file

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

View file

@ -252,7 +252,7 @@ module Mastodon::CLI
domain configuration. domain configuration.
LONG_DESC LONG_DESC
def fix_duplicates 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}") say("Duplicates found for #{uri}")
begin begin
ActivityPub::FetchRemoteAccountService.new.call(uri) unless dry_run? ActivityPub::FetchRemoteAccountService.new.call(uri) unless dry_run?

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,7 +13,6 @@ RSpec.describe Admin::DomainBlocksController do
it 'assigns a new domain block' do it 'assigns a new domain block' do
get :new get :new
expect(assigns(:domain_block)).to be_instance_of(DomainBlock)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
end end
@ -171,7 +170,6 @@ RSpec.describe Admin::DomainBlocksController do
it 'returns http success' do it 'returns http success' do
get :edit, params: { id: domain_block.id } get :edit, params: { id: domain_block.id }
expect(assigns(:domain_block)).to be_instance_of(DomainBlock)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
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') } } post :import, params: { admin_import: { data: fixture_file_upload('domain_blocks.csv') } }
end end
it 'renders page with expected domain blocks' do it 'renders page with expected domain blocks and returns http success' 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]) expect(mapped_batch_table_rows).to contain_exactly(['bad.domain', :silence], ['worse.domain', :suspend], ['reject.media', :noop])
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
end end
@ -56,14 +53,19 @@ RSpec.describe Admin::ExportDomainBlocksController do
post :import, params: { admin_import: { data: fixture_file_upload('domain_blocks_list.txt') } } post :import, params: { admin_import: { data: fixture_file_upload('domain_blocks_list.txt') } }
end end
it 'renders page with expected domain blocks' do it 'renders page with expected domain blocks and returns http success' 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]) expect(mapped_batch_table_rows).to contain_exactly(['bad.domain', :suspend], ['worse.domain', :suspend], ['reject.media', :suspend])
end
it 'returns http success' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
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 end
it 'displays error on no file selected' do it 'displays error on no file selected' do

View file

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

View file

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

View file

@ -13,39 +13,39 @@ describe Admin::ReportsController do
describe 'GET #index' do describe 'GET #index' do
it 'returns http success with no filters' do it 'returns http success with no filters' do
specified = Fabricate(:report, action_taken_at: nil) specified = Fabricate(:report, action_taken_at: nil, comment: 'First report')
Fabricate(:report, action_taken_at: Time.now.utc) other = Fabricate(:report, action_taken_at: Time.now.utc, comment: 'Second report')
get :index 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).to have_http_status(200)
expect(response.body)
.to include(specified.comment)
.and not_include(other.comment)
end end
it 'returns http success with resolved filter' do it 'returns http success with resolved filter' do
specified = Fabricate(:report, action_taken_at: Time.now.utc) specified = Fabricate(:report, action_taken_at: Time.now.utc, comment: 'First report')
Fabricate(:report, action_taken_at: nil) other = Fabricate(:report, action_taken_at: nil, comment: 'Second report')
get :index, params: { resolved: '1' } 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).to have_http_status(200)
expect(response.body)
.to include(specified.comment)
.and not_include(other.comment)
end end
end end
describe 'GET #show' do describe 'GET #show' do
it 'renders report' do it 'renders report' do
report = Fabricate(:report) report = Fabricate(:report, comment: 'A big problem')
get :show, params: { id: report } get :show, params: { id: report }
expect(assigns(:report)).to eq report
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.body)
.to include(report.comment)
end end
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 sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
end 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 describe 'PUT #update' do
it 'cannot create a setting value for a non-admin key' do it 'cannot create a setting value for a non-admin key' do
expect(Setting.new_setting_key).to be_blank 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(response).to redirect_to(admin_settings_branding_path)
expect(Setting.new_setting_key).to be_nil expect(Setting.new_setting_key).to be_nil
end 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 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' } get :show, params: { acct: 'http://example.com' }
expect(response).to have_http_status(302) expect(response)
expect(assigns(:resource)).to eq account .to have_http_status(302)
.and redirect_to(web_url("@#{account.pretty_acct}"))
end end
it 'sets resource from acct uri' do it 'sets resource from acct uri' do
@ -58,8 +59,9 @@ describe AuthorizeInteractionsController do
get :show, params: { acct: 'acct:found@hostname' } get :show, params: { acct: 'acct:found@hostname' }
expect(response).to have_http_status(302) expect(response)
expect(assigns(:resource)).to eq account .to have_http_status(302)
.and redirect_to(web_url("@#{account.pretty_acct}"))
end end
end end
end end

View file

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

View file

@ -21,9 +21,10 @@ RSpec.describe Settings::ImportsController do
it 'assigns the expected imports', :aggregate_failures do it 'assigns the expected imports', :aggregate_failures do
expect(response).to have_http_status(200) 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.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
end end
@ -261,7 +262,8 @@ RSpec.describe Settings::ImportsController do
it 'does not creates an unconfirmed bulk_import', :aggregate_failures do it 'does not creates an unconfirmed bulk_import', :aggregate_failures do
expect { subject }.to_not(change { user.account.bulk_imports.count }) 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
end end

View file

@ -9,11 +9,16 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
it 'renders the new view' do it 'renders the new view' do
subject 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 have_http_status(200)
expect(response).to render_template(:new) 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
end end
@ -61,10 +66,10 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
expect { post_create_with_options } expect { post_create_with_options }
.to change { user.reload.otp_secret }.to 'thisisasecretforthespecofnewview' .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(flash[:notice]).to eq 'Two-factor authentication successfully enabled'
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index') expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
expect(response.body).to include(*otp_backup_codes)
end end
end end

View file

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

View file

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

View file

@ -613,6 +613,25 @@ describe Mastodon::CLI::Accounts do
end end
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 describe '#backup' do
let(:action) { :backup } let(:action) { :backup }

View file

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

View file

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

View file

@ -3,6 +3,8 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Tag do RSpec.describe Tag do
include_examples 'Reviewable'
describe 'validations' do describe 'validations' do
it 'invalid with #' do it 'invalid with #' do
expect(described_class.new(name: '#hello_world')).to_not be_valid 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 describe '#rotate_secret!' do
it 'changes the secret' do it 'changes the secret' do
previous_value = webhook.secret expect { webhook.rotate_secret! }
webhook.rotate_secret! .to change(webhook, :secret)
expect(webhook.secret).to_not be_blank expect(webhook.secret)
expect(webhook.secret).to_not eq previous_value .to_not be_blank
end end
end end
describe '#enable!' do describe '#enable!' do
before do let(:webhook) { Fabricate(:webhook, enabled: false) }
webhook.disable!
end
it 'enables the webhook' do it 'enables the webhook' do
webhook.enable! expect { webhook.enable! }
expect(webhook.enabled?).to be true .to change(webhook, :enabled?).to(true)
end end
end end
describe '#disable!' do describe '#disable!' do
let(:webhook) { Fabricate(:webhook, enabled: true) }
it 'disables the webhook' do it 'disables the webhook' do
webhook.disable! expect { webhook.disable! }
expect(webhook.enabled?).to be false .to change(webhook, :enabled?).to(false)
end end
end end
end end

View file

@ -55,6 +55,8 @@ Sidekiq.logger = nil
DatabaseCleaner.strategy = [:deletion] DatabaseCleaner.strategy = [:deletion]
Chewy.settings[:enabled] = false
Devise::Test::ControllerHelpers.module_eval do Devise::Test::ControllerHelpers.module_eval do
alias_method :original_sign_in, :sign_in alias_method :original_sign_in, :sign_in
@ -112,6 +114,7 @@ RSpec.configure do |config|
config.include ThreadingHelpers config.include ThreadingHelpers
config.include SignedRequestHelpers, type: :request config.include SignedRequestHelpers, type: :request
config.include CommandLineHelpers, type: :cli config.include CommandLineHelpers, type: :cli
config.include SystemHelpers, type: :system
config.around(:each, use_transactional_tests: false) do |example| config.around(:each, use_transactional_tests: false) do |example|
self.use_transactional_tests = false self.use_transactional_tests = false
@ -128,6 +131,12 @@ RSpec.configure do |config|
example.run example.run
end 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 config.before :each, type: :cli do
stub_reset_connection_pools stub_reset_connection_pools
end end
@ -138,10 +147,19 @@ RSpec.configure do |config|
config.before do |example| config.before do |example|
unless example.metadata[:attachment_processing] 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
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 config.after do
Rails.cache.clear Rails.cache.clear
redis.del(redis.keys) 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:title')).to match alice.display_name
expect(head_meta_content('og:type')).to eq 'profile' expect(head_meta_content('og:type')).to eq 'profile'
expect(head_meta_content('og:image')).to match '.+' 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 end
def head_link_icons 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 it 'returns a JSON version of the account', :aggregate_failures do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
.and have_attributes( .and have_attributes(
media_type: eq('application/activity+json') 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) expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
context 'with authorized fetch mode' do context 'with authorized fetch mode' do
let(:authorized_fetch_mode) { true } 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 it 'returns a JSON version of the account', :aggregate_failures do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
.and have_attributes( .and have_attributes(
media_type: eq('application/activity+json') 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) expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
context 'with authorized fetch mode' do context 'with authorized fetch mode' do
let(:authorized_fetch_mode) { true } let(:authorized_fetch_mode) { true }
@ -215,10 +213,10 @@ describe 'Accounts show response' do
get short_account_path(username: account.username, format: format) get short_account_path(username: account.username, format: format)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses', :aggregate_failures do 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_media))
expect(response.body).to include(status_tag_for(status_self_reply)) expect(response.body).to include(status_tag_for(status_self_reply))
expect(response.body).to include(status_tag_for(status)) 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) get short_account_with_replies_path(username: account.username, format: format)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with replies', :aggregate_failures do 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_media))
expect(response.body).to include(status_tag_for(status_reply)) expect(response.body).to include(status_tag_for(status_reply))
expect(response.body).to include(status_tag_for(status_self_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) get short_account_media_path(username: account.username, format: format)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with media', :aggregate_failures do 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 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_direct))
expect(response.body).to_not include(status_tag_for(status_private)) 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) get short_account_tag_path(username: account.username, tag: tag, format: format)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with a tag', :aggregate_failures do 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 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_direct))
expect(response.body).to_not include(status_tag_for(status_media)) 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 Rails.configuration.x.streaming_api_base_url = before
end end
let(:headers) { { 'Host' => Rails.configuration.x.web_domain } }
context 'with streaming api on same host' do context 'with streaming api on same host' do
describe 'GET /api/v1/streaming' do describe 'GET /api/v1/streaming' do
it 'raises ActiveRecord::RecordNotFound' 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) expect(response).to have_http_status(404)
end 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(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } 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 describe 'GET /api/v1/timelines/tag/:hashtag' do
subject do subject do
get "/api/v1/timelines/tag/#{hashtag}", headers: headers, params: params get "/api/v1/timelines/tag/#{hashtag}", headers: headers, params: params
@ -26,8 +17,20 @@ RSpec.describe 'Tag' do
Setting.timeline_preview = true Setting.timeline_preview = true
end 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(: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!(: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!(:war_status) { PostStatusService.new.call(user.account, text: '#war, war never changes') }
let!(:love_status) { PostStatusService.new.call(account, text: 'what is #love?') } 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 it 'returns http success' do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers
.and have_attributes( .and have_attributes(
content_type: match('text/css') content_type: match('text/css')
) )
end end
it_behaves_like 'cacheable response'
end end
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 it 'returns http success with correct media type and body' do
expect(response) expect(response)
.to have_http_status(200) .to have_http_status(200)
.and have_cacheable_headers
expect(response.content_type) expect(response.content_type)
.to start_with('application/activity+json') .to start_with('application/activity+json')
expect(body_as_json) expect(body_as_json)
@ -32,8 +33,6 @@ RSpec.describe 'Instance actor endpoint' do
url: about_more_url(instance_actor: true) url: about_more_url(instance_actor: true)
) )
end end
it_behaves_like 'cacheable response'
end end
context 'with limited federation mode disabled' do 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 it 'contains webfinger url in link header' do
link_header = link_header_with_type('application/jrd+json') 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) expect(link_header.attr_pairs.first).to eq %w(rel lrdd)
end end

View file

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

View file

@ -4,12 +4,7 @@ require 'rails_helper'
describe 'Media Proxy' do describe 'Media Proxy' do
describe 'GET /media_proxy/:id' do describe 'GET /media_proxy/:id' do
before do before { stub_attachment_request }
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
context 'when attached to a status' do context 'when attached to a status' do
let(:status) { Fabricate(:status) } let(:status) { Fabricate(:status) }
@ -63,5 +58,15 @@ describe 'Media Proxy' do
.to have_http_status(404) .to have_http_status(404)
end end
end end
def stub_attachment_request
stub_request(
:get,
'http://example.com/attachment.png'
)
.to_return(
request_fixture('avatar.txt')
)
end
end 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| config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true 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 end
config.before :suite do 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' 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(:alice) { Fabricate(:account, username: 'alice', display_name: 'Alice') }
let(:status) { Fabricate(:status, account: alice, text: 'Hello World') } let(:status) { Fabricate(:status, account: alice, text: 'Hello World') }
before do 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) view.extend view_helpers
allow(view).to receive(:local_time)
allow(view).to receive(:local_time_ago)
assign(:instance_presenter, InstancePresenter.new) assign(:instance_presenter, InstancePresenter.new)
Fabricate(:media_attachment, account: alice, status: status, type: :video) 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 def header_tags
view.content_for(:header_tags) view.content_for(:header_tags)
end 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 end