diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 4e55ba3080..96317a64dc 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -394,7 +394,7 @@ RSpec/MessageSpies:
- 'spec/validators/status_length_validator_spec.rb'
RSpec/MultipleExpectations:
- Max: 19
+ Max: 8
# Configuration parameters: AllowSubject.
RSpec/MultipleMemoizedHelpers:
@@ -1242,11 +1242,6 @@ Style/GlobalStdStream:
- 'config/environments/development.rb'
- 'config/environments/production.rb'
-# Configuration parameters: AllowedVariables.
-Style/GlobalVars:
- Exclude:
- - 'config/initializers/statsd.rb'
-
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
Style/GuardClause:
@@ -1380,7 +1375,6 @@ Style/RedundantConstantBase:
Exclude:
- 'config/environments/production.rb'
- 'config/initializers/sidekiq.rb'
- - 'config/initializers/statsd.rb'
- 'config/locales/sr-Latn.rb'
- 'config/locales/sr.rb'
diff --git a/Gemfile.lock b/Gemfile.lock
index 192ff5c41a..c4fa1822ea 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -887,4 +887,4 @@ RUBY VERSION
ruby 3.2.2p53
BUNDLED WITH
- 2.4.10
+ 2.4.13
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 099512248f..3a6df662ea 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -14,15 +14,5 @@ module Admin
@pending_tags_count = Tag.pending_review.count
@pending_appeals_count = Appeal.pending.count
end
-
- private
-
- def redis_info
- @redis_info ||= if redis.is_a?(Redis::Namespace)
- redis.redis.info
- else
- redis.info
- end
- end
end
end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 8af4242ba3..ddb94d5ca4 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -90,7 +90,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def account_params
- params.permit(:username, :email, :password, :agreement, :locale, :reason)
+ params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone)
end
def check_enabled_registrations
diff --git a/app/controllers/mail_subscriptions_controller.rb b/app/controllers/mail_subscriptions_controller.rb
new file mode 100644
index 0000000000..b071a80605
--- /dev/null
+++ b/app/controllers/mail_subscriptions_controller.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+class MailSubscriptionsController < ApplicationController
+ layout 'auth'
+
+ skip_before_action :require_functional!
+
+ before_action :set_body_classes
+ before_action :set_user
+ before_action :set_type
+
+ def show; end
+
+ def create
+ @user.settings[email_type_from_param] = false
+ @user.save!
+ end
+
+ private
+
+ def set_user
+ @user = GlobalID::Locator.locate_signed(params[:token], for: 'unsubscribe')
+ end
+
+ def set_body_classes
+ @body_classes = 'lighter'
+ end
+
+ def set_type
+ @type = email_type_from_param
+ end
+
+ def email_type_from_param
+ case params[:type]
+ when 'follow', 'reblog', 'favourite', 'mention', 'follow_request'
+ "notification_emails.#{params[:type]}"
+ else
+ raise ArgumentError
+ end
+ end
+end
diff --git a/app/controllers/settings/preferences/base_controller.rb b/app/controllers/settings/preferences/base_controller.rb
index faf778a7e5..c1f8b49898 100644
--- a/app/controllers/settings/preferences/base_controller.rb
+++ b/app/controllers/settings/preferences/base_controller.rb
@@ -19,6 +19,6 @@ class Settings::Preferences::BaseController < Settings::BaseController
end
def user_params
- params.require(:user).permit(:locale, chosen_languages: [], settings_attributes: UserSettings.keys)
+ params.require(:user).permit(:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys)
end
end
diff --git a/app/javascript/mastodon/components/load_pending.jsx b/app/javascript/mastodon/components/load_pending.jsx
deleted file mode 100644
index e9c1a97836..0000000000
--- a/app/javascript/mastodon/components/load_pending.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { FormattedMessage } from 'react-intl';
-
-export default class LoadPending extends PureComponent {
-
- static propTypes = {
- onClick: PropTypes.func,
- count: PropTypes.number,
- };
-
- render() {
- const { count } = this.props;
-
- return (
-
- );
- }
-
-}
diff --git a/app/javascript/mastodon/components/load_pending.tsx b/app/javascript/mastodon/components/load_pending.tsx
new file mode 100644
index 0000000000..f7589622ed
--- /dev/null
+++ b/app/javascript/mastodon/components/load_pending.tsx
@@ -0,0 +1,18 @@
+import { FormattedMessage } from 'react-intl';
+
+interface Props {
+ onClick: (event: React.MouseEvent) => void;
+ count: number;
+}
+
+export const LoadPending: React.FC = ({ onClick, count }) => {
+ return (
+
+ );
+};
diff --git a/app/javascript/mastodon/components/scrollable_list.jsx b/app/javascript/mastodon/components/scrollable_list.jsx
index 53a84ecb53..5c990d6dca 100644
--- a/app/javascript/mastodon/components/scrollable_list.jsx
+++ b/app/javascript/mastodon/components/scrollable_list.jsx
@@ -16,7 +16,7 @@ import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { LoadMore } from './load_more';
-import LoadPending from './load_pending';
+import { LoadPending } from './load_pending';
import LoadingIndicator from './loading_indicator';
const MOUSE_IDLE_DELAY = 300;
diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx
index e4d69730da..3288fd73da 100644
--- a/app/javascript/mastodon/features/account/components/header.jsx
+++ b/app/javascript/mastodon/features/account/components/header.jsx
@@ -377,7 +377,7 @@ class Header extends ImmutablePureComponent {
let badge;
if (account.get('bot')) {
- badge = (
);
+ badge = (
);
} else if (account.get('group')) {
badge = (
);
} else {
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 8a23e39178..2205dcbf94 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -15,7 +15,7 @@
"about.rules": "Server rules",
"account.account_note_header": "Note",
"account.add_or_remove_from_list": "Add or Remove from lists",
- "account.badges.bot": "Bot",
+ "account.badges.bot": "Automated",
"account.badges.group": "Group",
"account.block": "Block @{name}",
"account.block_domain": "Block domain {domain}",
diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
index c428fd30d6..7cd3bab1af 100644
--- a/app/mailers/notification_mailer.rb
+++ b/app/mailers/notification_mailer.rb
@@ -8,61 +8,71 @@ class NotificationMailer < ApplicationMailer
def mention(recipient, notification)
@me = recipient
+ @user = recipient.user
+ @type = 'mention'
@status = notification.target_status
- return unless @me.user.functional? && @status.present?
+ return unless @user.functional? && @status.present?
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
- mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
+ mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
end
end
def follow(recipient, notification)
@me = recipient
+ @user = recipient.user
+ @type = 'follow'
@account = notification.from_account
- return unless @me.user.functional?
+ return unless @user.functional?
locale_for_account(@me) do
- mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
+ mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
end
end
def favourite(recipient, notification)
@me = recipient
+ @user = recipient.user
+ @type = 'favourite'
@account = notification.from_account
@status = notification.target_status
- return unless @me.user.functional? && @status.present?
+ return unless @user.functional? && @status.present?
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
- mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
+ mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
end
end
def reblog(recipient, notification)
@me = recipient
+ @user = recipient.user
+ @type = 'reblog'
@account = notification.from_account
@status = notification.target_status
- return unless @me.user.functional? && @status.present?
+ return unless @user.functional? && @status.present?
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
- mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
+ mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
end
end
def follow_request(recipient, notification)
@me = recipient
+ @user = recipient.user
+ @type = 'follow_request'
@account = notification.from_account
- return unless @me.user.functional?
+ return unless @user.functional?
locale_for_account(@me) do
- mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
+ mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
end
end
diff --git a/app/models/account.rb b/app/models/account.rb
index 4f877efb36..2e166fee98 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -145,6 +145,7 @@ class Account < ApplicationRecord
:locale,
:shows_application?,
:prefers_noindex?,
+ :time_zone,
to: :user,
prefix: true,
allow_nil: true
diff --git a/app/models/user.rb b/app/models/user.rb
index a0678117d4..83c7d5e356 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -40,6 +40,7 @@
# sign_up_ip :inet
# role_id :bigint(8)
# settings :text
+# time_zone :string
#
class User < ApplicationRecord
@@ -101,6 +102,7 @@ class User < ApplicationRecord
validates_with BlacklistedEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }
validates_with EmailMxValidator, if: :validate_email_dns?
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
+ validates :time_zone, inclusion: { in: ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.name } }, allow_blank: true
# Honeypot/anti-spam fields
attr_accessor :registration_form_time, :website, :confirm_password
diff --git a/app/services/app_sign_up_service.rb b/app/services/app_sign_up_service.rb
index 3833327bbc..94547b61b2 100644
--- a/app/services/app_sign_up_service.rb
+++ b/app/services/app_sign_up_service.rb
@@ -35,7 +35,7 @@ class AppSignUpService < BaseService
end
def user_params
- @params.slice(:email, :password, :agreement, :locale)
+ @params.slice(:email, :password, :agreement, :locale, :time_zone)
end
def account_params
diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml
index 43c8559270..e39a09780e 100644
--- a/app/views/layouts/mailer.html.haml
+++ b/app/views/layouts/mailer.html.haml
@@ -44,7 +44,11 @@
%tbody
%td.column-cell
%p= t 'about.hosted_on', domain: site_hostname
- %p= link_to t('application_mailer.notification_preferences'), settings_preferences_notifications_url
+ %p
+ = link_to t('application_mailer.notification_preferences'), settings_preferences_notifications_url
+ - if defined?(@type)
+ ยท
+ = link_to t('application_mailer.unsubscribe'), unsubscribe_url(token: @user.to_sgid(for: 'unsubscribe').to_s, type: @type)
%td.column-cell.text-right
= link_to root_url do
= image_tag full_pack_url('media/images/mailer/logo.png'), alt: 'Mastodon', height: 24
diff --git a/app/views/mail_subscriptions/create.html.haml b/app/views/mail_subscriptions/create.html.haml
new file mode 100644
index 0000000000..16ee486b00
--- /dev/null
+++ b/app/views/mail_subscriptions/create.html.haml
@@ -0,0 +1,9 @@
+- content_for :page_title do
+ = t('mail_subscriptions.unsubscribe.title')
+
+.simple_form
+ %h1.title= t('mail_subscriptions.unsubscribe.complete')
+ %p.lead
+ = t('mail_subscriptions.unsubscribe.success_html', domain: content_tag(:strong, site_hostname), type: content_tag(:strong, I18n.t(@type, scope: 'mail_subscriptions.unsubscribe.emails')), email: content_tag(:strong, @user.email))
+ %p.lead
+ = t('mail_subscriptions.unsubscribe.resubscribe_html', settings_path: settings_preferences_notifications_path)
diff --git a/app/views/mail_subscriptions/show.html.haml b/app/views/mail_subscriptions/show.html.haml
new file mode 100644
index 0000000000..afa2ab6ed7
--- /dev/null
+++ b/app/views/mail_subscriptions/show.html.haml
@@ -0,0 +1,12 @@
+- content_for :page_title do
+ = t('mail_subscriptions.unsubscribe.title')
+
+.simple_form
+ %h1.title= t('mail_subscriptions.unsubscribe.title')
+ %p.lead
+ = t('mail_subscriptions.unsubscribe.confirmation_html', domain: content_tag(:strong, site_hostname), type: content_tag(:strong, I18n.t(@type, scope: 'mail_subscriptions.unsubscribe.emails')), email: content_tag(:strong, @user.email), settings_path: settings_preferences_notifications_path)
+
+ = form_tag unsubscribe_path, method: :post do
+ = hidden_field_tag :token, params[:token]
+ = hidden_field_tag :type, params[:type]
+ = button_tag t('mail_subscriptions.unsubscribe.action'), type: :submit
diff --git a/app/views/notification_mailer/_status.html.haml b/app/views/notification_mailer/_status.html.haml
index fd65039ae9..d8aa38b112 100644
--- a/app/views/notification_mailer/_status.html.haml
+++ b/app/views/notification_mailer/_status.html.haml
@@ -42,4 +42,4 @@
= link_to a.remote_url, a.remote_url
%p.status-footer
- = link_to l(status.created_at), web_url("@#{status.account.pretty_acct}/#{status.id}")
+ = link_to l(status.created_at.in_time_zone(time_zone)), web_url("@#{status.account.pretty_acct}/#{status.id}")
diff --git a/app/views/notification_mailer/favourite.html.haml b/app/views/notification_mailer/favourite.html.haml
index 4ec89172d9..325f0aff5f 100644
--- a/app/views/notification_mailer/favourite.html.haml
+++ b/app/views/notification_mailer/favourite.html.haml
@@ -22,7 +22,7 @@
%h1= t 'notification_mailer.favourite.title'
%p.lead= t('notification_mailer.favourite.body', name: @account.pretty_acct)
-= render 'status', status: @status
+= render 'status', status: @status, time_zone: @me.user_time_zone
%table.email-table{ cellspacing: 0, cellpadding: 0 }
%tbody
diff --git a/app/views/notification_mailer/mention.html.haml b/app/views/notification_mailer/mention.html.haml
index 4ae9bb7b0b..e830644c3f 100644
--- a/app/views/notification_mailer/mention.html.haml
+++ b/app/views/notification_mailer/mention.html.haml
@@ -22,7 +22,7 @@
%h1= t 'notification_mailer.mention.title'
%p.lead= t('notification_mailer.mention.body', name: @status.account.pretty_acct)
-= render 'status', status: @status
+= render 'status', status: @status, time_zone: @me.user_time_zone
%table.email-table{ cellspacing: 0, cellpadding: 0 }
%tbody
diff --git a/app/views/notification_mailer/reblog.html.haml b/app/views/notification_mailer/reblog.html.haml
index f805c79f0d..e4f9441236 100644
--- a/app/views/notification_mailer/reblog.html.haml
+++ b/app/views/notification_mailer/reblog.html.haml
@@ -22,7 +22,7 @@
%h1= t 'notification_mailer.reblog.title'
%p.lead= t('notification_mailer.reblog.body', name: @account.pretty_acct)
-= render 'status', status: @status
+= render 'status', status: @status, time_zone: @me.user_time_zone
%table.email-table{ cellspacing: 0, cellpadding: 0 }
%tbody
diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml
index 43c3be19df..d0dc221123 100644
--- a/app/views/settings/preferences/appearance/show.html.haml
+++ b/app/views/settings/preferences/appearance/show.html.haml
@@ -9,8 +9,11 @@
.fields-group.fields-row__column.fields-row__column-6
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| native_locale_name(locale) }, selected: I18n.locale, hint: false
.fields-group.fields-row__column.fields-row__column-6
- = f.simple_fields_for :settings, current_user.settings do |ff|
- = ff.input :theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false, hint: false
+ = f.input :time_zone, wrapper: :with_label, collection: ActiveSupport::TimeZone.all.map { |tz| ["(GMT#{tz.formatted_offset}) #{tz.name}", tz.tzinfo.name] }, hint: false
+
+ .fields-group
+ = f.simple_fields_for :settings, current_user.settings do |ff|
+ = ff.input :theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_theme'), include_blank: false, hint: false
- unless I18n.locale == :en
.flash-message.translation-prompt
diff --git a/app/views/user_mailer/appeal_approved.html.haml b/app/views/user_mailer/appeal_approved.html.haml
index 962cab2e2c..d62789a067 100644
--- a/app/views/user_mailer/appeal_approved.html.haml
+++ b/app/views/user_mailer/appeal_approved.html.haml
@@ -36,7 +36,7 @@
%tbody
%tr
%td.column-cell.text-center
- %p= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at)
+ %p= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at.in_time_zone(@resource.time_zone)), strike_date: l(@appeal.strike.created_at.in_time_zone(@resource.time_zone))
%table.email-table{ cellspacing: 0, cellpadding: 0 }
%tbody
diff --git a/app/views/user_mailer/appeal_approved.text.erb b/app/views/user_mailer/appeal_approved.text.erb
index 290fa24c36..99596605aa 100644
--- a/app/views/user_mailer/appeal_approved.text.erb
+++ b/app/views/user_mailer/appeal_approved.text.erb
@@ -2,6 +2,6 @@
===
-<%= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at) %>
+<%= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at.in_time_zone(@resource.time_zone)), strike_date: l(@appeal.strike.created_at.in_time_zone(@resource.time_zone)) %>
=> <%= root_url %>
diff --git a/app/views/user_mailer/appeal_rejected.html.haml b/app/views/user_mailer/appeal_rejected.html.haml
index c316a73fb5..ae60775b01 100644
--- a/app/views/user_mailer/appeal_rejected.html.haml
+++ b/app/views/user_mailer/appeal_rejected.html.haml
@@ -36,7 +36,7 @@
%tbody
%tr
%td.column-cell.text-center
- %p= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at)
+ %p= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at.in_time_zone(@resource.time_zone)), strike_date: l(@appeal.strike.created_at.in_time_zone(@resource.time_zone))
%table.email-table{ cellspacing: 0, cellpadding: 0 }
%tbody
diff --git a/app/views/user_mailer/appeal_rejected.text.erb b/app/views/user_mailer/appeal_rejected.text.erb
index f47a768181..3c93777180 100644
--- a/app/views/user_mailer/appeal_rejected.text.erb
+++ b/app/views/user_mailer/appeal_rejected.text.erb
@@ -2,6 +2,6 @@
===
-<%= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at) %>
+<%= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at.in_time_zone(@resource.time_zone)), strike_date: l(@appeal.strike.created_at.in_time_zone(@resource.time_zone)) %>
=> <%= root_url %>
diff --git a/app/views/user_mailer/suspicious_sign_in.html.haml b/app/views/user_mailer/suspicious_sign_in.html.haml
index e4ad500c3d..6ebba3fa55 100644
--- a/app/views/user_mailer/suspicious_sign_in.html.haml
+++ b/app/views/user_mailer/suspicious_sign_in.html.haml
@@ -47,7 +47,7 @@
%strong= "#{t('sessions.browser')}:"
%span{ title: @user_agent }= t 'sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: @detection.id.to_s), platform: t("sessions.platforms.#{@detection.platform.id}", default: @detection.platform.id.to_s)
%br/
- = l(@timestamp)
+ = l(@timestamp.in_time_zone(@resource.time_zone))
%table.email-table{ cellspacing: 0, cellpadding: 0 }
%tbody
diff --git a/app/views/user_mailer/suspicious_sign_in.text.erb b/app/views/user_mailer/suspicious_sign_in.text.erb
index 7d2ca28e84..956071e774 100644
--- a/app/views/user_mailer/suspicious_sign_in.text.erb
+++ b/app/views/user_mailer/suspicious_sign_in.text.erb
@@ -8,7 +8,7 @@
<%= t('sessions.ip') %>: <%= @remote_ip %>
<%= t('sessions.browser') %>: <%= t('sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")) %>
-<%= l(@timestamp) %>
+<%= l(@timestamp.in_time_zone(@resource.time_zone)) %>
<%= t 'user_mailer.suspicious_sign_in.further_actions_html', action: t('user_mailer.suspicious_sign_in.change_password') %>
diff --git a/app/views/user_mailer/warning.html.haml b/app/views/user_mailer/warning.html.haml
index b9422e9502..9cb73b0fee 100644
--- a/app/views/user_mailer/warning.html.haml
+++ b/app/views/user_mailer/warning.html.haml
@@ -58,7 +58,7 @@
- unless @statuses.empty?
- @statuses.each_with_index do |status, i|
- = render 'notification_mailer/status', status: status, i: i + 1, highlighted: true
+ = render 'notification_mailer/status', status: status, i: i + 1, highlighted: true, time_zone: @resource.time_zone
%table.email-table{ cellspacing: 0, cellpadding: 0 }
%tbody
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index ddf503197c..035a0e999d 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -66,6 +66,7 @@ ignore_unused:
- 'notification_mailer.*'
- 'imports.overwrite_preambles.{following,blocking,muting,domain_blocking,bookmarks}_html'
- 'imports.preambles.{following,blocking,muting,domain_blocking,bookmarks}_html'
+ - 'mail_subscriptions.unsubscribe.emails.*'
ignore_inconsistent_interpolations:
- '*.one'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index db2712cd8a..ff51109b00 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1072,6 +1072,7 @@ en:
notification_preferences: Change e-mail preferences
salutation: "%{name},"
settings: 'Change e-mail preferences: %{link}'
+ unsubscribe: Unsubscribe
view: 'View:'
view_profile: View profile
view_status: View post
@@ -1437,6 +1438,21 @@ en:
failed_sign_in_html: Failed sign-in attempt with %{method} from %{ip} (%{browser})
successful_sign_in_html: Successful sign-in with %{method} from %{ip} (%{browser})
title: Authentication history
+ mail_subscriptions:
+ unsubscribe:
+ action: Yes, unsubscribe
+ complete: Unsubscribed
+ confirmation_html: Are you sure you want to unsubscribe from receiving %{type} for Mastodon on %{domain} to your e-mail at %{email}? You can always re-subscribe from your e-mail notification settings.
+ emails:
+ notification_emails:
+ favourite: favorite notification e-mails
+ follow: follow notification e-mails
+ follow_request: follow request e-mails
+ mention: mention notification e-mails
+ reblog: boost notification e-mails
+ resubscribe_html: If you've unsubscribed by mistake, you can re-subscribe from your e-mail notification settings.
+ success_html: You'll no longer receive %{type} for Mastodon on %{domain} to your e-mail at %{email}.
+ title: Unsubscribe
media_attachments:
validations:
images_and_video: Cannot attach a video to a post that already contains images
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 40b3e97161..cd2dcfb3cc 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -173,7 +173,7 @@ en:
defaults:
autofollow: Invite to follow your account
avatar: Avatar
- bot: This is a bot account
+ bot: This is an automated account
chosen_languages: Filter languages
confirm_new_password: Confirm new password
confirm_password: Confirm password
@@ -322,6 +322,7 @@ en:
usable: Allow posts to use this hashtag
user:
role: Role
+ time_zone: Time zone
user_role:
color: Badge color
highlighted: Display role as badge on user profiles
diff --git a/config/routes.rb b/config/routes.rb
index e69f964211..4a0eb12b4d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -69,6 +69,8 @@ Rails.application.routes.draw do
devise_scope :user do
get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite
+ resource :unsubscribe, only: [:show, :create], controller: :mail_subscriptions
+
namespace :auth do
resource :setup, only: [:show, :update], controller: :setup
resource :challenge, only: [:create], controller: :challenges
diff --git a/db/migrate/20230605085711_add_time_zone_to_users.rb b/db/migrate/20230605085711_add_time_zone_to_users.rb
new file mode 100644
index 0000000000..fc6c0b091c
--- /dev/null
+++ b/db/migrate/20230605085711_add_time_zone_to_users.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddTimeZoneToUsers < ActiveRecord::Migration[6.1]
+ def change
+ add_column :users, :time_zone, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 68a95c345e..9169d163db 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -12,7 +12,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2023_06_05_085710) do
+ActiveRecord::Schema.define(version: 2023_06_05_085711) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1211,6 +1211,7 @@ ActiveRecord::Schema.define(version: 2023_06_05_085710) do
t.boolean "skip_sign_in_token"
t.bigint "role_id"
t.text "settings"
+ t.string "time_zone"
t.index ["account_id"], name: "index_users_on_account_id"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)"
diff --git a/lib/mastodon/cli/cache.rb b/lib/mastodon/cli/cache.rb
index 3097a99678..c88ab935d6 100644
--- a/lib/mastodon/cli/cache.rb
+++ b/lib/mastodon/cli/cache.rb
@@ -23,23 +23,12 @@ module Mastodon::CLI
def recount(type)
case type
when 'accounts'
- processed, = parallelize_with_progress(Account.local.includes(:account_stat)) do |account|
- account_stat = account.account_stat
- account_stat.following_count = account.active_relationships.count
- account_stat.followers_count = account.passive_relationships.count
- account_stat.statuses_count = account.statuses.where.not(visibility: :direct).count
-
- account_stat.save if account_stat.changed?
+ processed, = parallelize_with_progress(accounts_with_stats) do |account|
+ recount_account_stats(account)
end
when 'statuses'
- processed, = parallelize_with_progress(Status.includes(:status_stat)) do |status|
- status_stat = status.status_stat
- status_stat.replies_count = status.replies.where.not(visibility: :direct).count
- status_stat.reblogs_count = status.reblogs.count
- status_stat.favourites_count = status.favourites.count
- status_stat.emoji_reactions = status.generate_emoji_reactions_grouped_by_name
-
- status_stat.save if status_stat.changed?
+ processed, = parallelize_with_progress(statuses_with_stats) do |status|
+ recount_status_stats(status)
end
else
say("Unknown type: #{type}", :red)
@@ -49,5 +38,36 @@ module Mastodon::CLI
say
say("OK, recounted #{processed} records", :green)
end
+
+ private
+
+ def accounts_with_stats
+ Account.local.includes(:account_stat)
+ end
+
+ def statuses_with_stats
+ Status.includes(:status_stat)
+ end
+
+ def recount_account_stats(account)
+ account.account_stat.tap do |account_stat|
+ account_stat.following_count = account.active_relationships.count
+ account_stat.followers_count = account.passive_relationships.count
+ account_stat.statuses_count = account.statuses.where.not(visibility: :direct).count
+
+ account_stat.save if account_stat.changed?
+ end
+ end
+
+ def recount_status_stats(status)
+ status.status_stat.tap do |status_stat|
+ status_stat.replies_count = status.replies.where.not(visibility: :direct).count
+ status_stat.reblogs_count = status.reblogs.count
+ status_stat.favourites_count = status.favourites.count
+ status_stat.emoji_reactions = status.generate_emoji_reactions_grouped_by_name
+
+ status_stat.save if status_stat.changed?
+ end
+ end
end
end
diff --git a/lib/mastodon/cli/feeds.rb b/lib/mastodon/cli/feeds.rb
index 34617e7538..3467dd427c 100644
--- a/lib/mastodon/cli/feeds.rb
+++ b/lib/mastodon/cli/feeds.rb
@@ -19,7 +19,7 @@ module Mastodon::CLI
LONG_DESC
def build(username = nil)
if options[:all] || username.nil?
- processed, = parallelize_with_progress(Account.joins(:user).merge(User.active)) do |account|
+ processed, = parallelize_with_progress(active_user_accounts) do |account|
PrecomputeFeedService.new.call(account) unless dry_run?
end
@@ -47,5 +47,11 @@ module Mastodon::CLI
redis.del(keys)
say('OK', :green)
end
+
+ private
+
+ def active_user_accounts
+ Account.joins(:user).merge(User.active)
+ end
end
end
diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
index 6bc07fa9e0..993ead636a 100644
--- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
@@ -48,25 +48,32 @@ describe Api::V1::Accounts::RelationshipsController do
expect(response).to have_http_status(200)
end
- it 'returns JSON with correct data' do
- json = body_as_json
+ context 'when there is returned JSON data' do
+ let(:json) { body_as_json }
- expect(json).to be_a Enumerable
- expect(json.first[:id]).to eq simon.id.to_s
- expect(json.first[:following]).to be true
- expect(json.first[:showing_reblogs]).to be true
- expect(json.first[:followed_by]).to be false
- expect(json.first[:muting]).to be false
- expect(json.first[:requested]).to be false
- expect(json.first[:domain_blocking]).to be false
+ it 'returns an enumerable json' do
+ expect(json).to be_a Enumerable
+ end
- expect(json.second[:id]).to eq lewis.id.to_s
- expect(json.second[:following]).to be false
- expect(json.second[:showing_reblogs]).to be false
- expect(json.second[:followed_by]).to be true
- expect(json.second[:muting]).to be false
- expect(json.second[:requested]).to be false
- expect(json.second[:domain_blocking]).to be false
+ it 'returns a correct first element' do
+ expect(json.first[:id]).to eq simon.id.to_s
+ expect(json.first[:following]).to be true
+ expect(json.first[:showing_reblogs]).to be true
+ expect(json.first[:followed_by]).to be false
+ expect(json.first[:muting]).to be false
+ expect(json.first[:requested]).to be false
+ expect(json.first[:domain_blocking]).to be false
+ end
+
+ it 'returns a correct second element' do
+ expect(json.second[:id]).to eq lewis.id.to_s
+ expect(json.second[:following]).to be false
+ expect(json.second[:showing_reblogs]).to be false
+ expect(json.second[:followed_by]).to be true
+ expect(json.second[:muting]).to be false
+ expect(json.second[:requested]).to be false
+ expect(json.second[:domain_blocking]).to be false
+ end
end
it 'returns JSON with correct data on cached requests too' do
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index 992fb0e893..49d2867745 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -55,20 +55,6 @@ RSpec.describe Api::V1::AccountsController do
end
end
- describe 'GET #show' do
- let(:scopes) { 'read:accounts' }
-
- before do
- get :show, params: { id: user.account.id }
- end
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- end
-
describe 'POST #follow' do
let(:scopes) { 'write:follows' }
let(:other_account) { Fabricate(:account, username: 'bob', locked: locked) }
diff --git a/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb
deleted file mode 100644
index fe39596dfd..0000000000
--- a/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb
+++ /dev/null
@@ -1,358 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Api::V1::Admin::CanonicalEmailBlocksController do
- render_views
-
- let(:role) { UserRole.find_by(name: 'Admin') }
- let(:user) { Fabricate(:user, role: role) }
- let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
- let(:scopes) { 'admin:read:canonical_email_blocks admin:write:canonical_email_blocks' }
-
- before do
- allow(controller).to receive(:doorkeeper_token) { token }
- end
-
- shared_examples 'forbidden for wrong scope' do |wrong_scope|
- let(:scopes) { wrong_scope }
-
- it 'returns http forbidden' do
- expect(response).to have_http_status(403)
- end
- end
-
- shared_examples 'forbidden for wrong role' do |wrong_role|
- let(:role) { UserRole.find_by(name: wrong_role) }
-
- it 'returns http forbidden' do
- expect(response).to have_http_status(403)
- end
- end
-
- describe 'GET #index' do
- context 'with wrong scope' do
- before do
- get :index
- end
-
- it_behaves_like 'forbidden for wrong scope', 'read:statuses'
- end
-
- context 'with wrong role' do
- before do
- get :index
- end
-
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
- end
-
- it 'returns http success' do
- get :index
-
- expect(response).to have_http_status(200)
- end
-
- context 'when there is no canonical email block' do
- it 'returns an empty list' do
- get :index
-
- body = body_as_json
-
- expect(body).to be_empty
- end
- end
-
- context 'when there are canonical email blocks' do
- let!(:canonical_email_blocks) { Fabricate.times(5, :canonical_email_block) }
- let(:expected_email_hashes) { canonical_email_blocks.pluck(:canonical_email_hash) }
-
- it 'returns the correct canonical email hashes' do
- get :index
-
- json = body_as_json
-
- expect(json.pluck(:canonical_email_hash)).to match_array(expected_email_hashes)
- end
-
- context 'with limit param' do
- let(:params) { { limit: 2 } }
-
- it 'returns only the requested number of canonical email blocks' do
- get :index, params: params
-
- json = body_as_json
-
- expect(json.size).to eq(params[:limit])
- end
- end
-
- context 'with since_id param' do
- let(:params) { { since_id: canonical_email_blocks[1].id } }
-
- it 'returns only the canonical email blocks after since_id' do
- get :index, params: params
-
- canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s)
- json = body_as_json
-
- expect(json.pluck(:id)).to match_array(canonical_email_blocks_ids[2..])
- end
- end
-
- context 'with max_id param' do
- let(:params) { { max_id: canonical_email_blocks[3].id } }
-
- it 'returns only the canonical email blocks before max_id' do
- get :index, params: params
-
- canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s)
- json = body_as_json
-
- expect(json.pluck(:id)).to match_array(canonical_email_blocks_ids[..2])
- end
- end
- end
- end
-
- describe 'GET #show' do
- let!(:canonical_email_block) { Fabricate(:canonical_email_block) }
- let(:params) { { id: canonical_email_block.id } }
-
- context 'with wrong scope' do
- before do
- get :show, params: params
- end
-
- it_behaves_like 'forbidden for wrong scope', 'read:statuses'
- end
-
- context 'with wrong role' do
- before do
- get :show, params: params
- end
-
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
- end
-
- context 'when canonical email block exists' do
- it 'returns http success' do
- get :show, params: params
-
- expect(response).to have_http_status(200)
- end
-
- it 'returns canonical email block data correctly' do
- get :show, params: params
-
- json = body_as_json
-
- expect(json[:id]).to eq(canonical_email_block.id.to_s)
- expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
- end
- end
-
- context 'when canonical block does not exist' do
- it 'returns http not found' do
- get :show, params: { id: 0 }
-
- expect(response).to have_http_status(404)
- end
- end
- end
-
- describe 'POST #test' do
- context 'with wrong scope' do
- before do
- post :test
- end
-
- it_behaves_like 'forbidden for wrong scope', 'read:statuses'
- end
-
- context 'with wrong role' do
- before do
- post :test, params: { email: 'whatever@email.com' }
- end
-
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
- end
-
- context 'when required email is not provided' do
- it 'returns http bad request' do
- post :test
-
- expect(response).to have_http_status(400)
- end
- end
-
- context 'when required email is provided' do
- let(:params) { { email: 'example@email.com' } }
-
- context 'when there is a matching canonical email block' do
- let!(:canonical_email_block) { CanonicalEmailBlock.create(params) }
-
- it 'returns http success' do
- post :test, params: params
-
- expect(response).to have_http_status(200)
- end
-
- it 'returns expected canonical email hash' do
- post :test, params: params
-
- json = body_as_json
-
- expect(json[0][:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
- end
- end
-
- context 'when there is no matching canonical email block' do
- it 'returns http success' do
- post :test, params: params
-
- expect(response).to have_http_status(200)
- end
-
- it 'returns an empty list' do
- post :test, params: params
-
- json = body_as_json
-
- expect(json).to be_empty
- end
- end
- end
- end
-
- describe 'POST #create' do
- let(:params) { { email: 'example@email.com' } }
- let(:canonical_email_block) { CanonicalEmailBlock.new(email: params[:email]) }
-
- context 'with wrong scope' do
- before do
- post :create, params: params
- end
-
- it_behaves_like 'forbidden for wrong scope', 'read:statuses'
- end
-
- context 'with wrong role' do
- before do
- post :create, params: params
- end
-
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
- end
-
- it 'returns http success' do
- post :create, params: params
-
- expect(response).to have_http_status(200)
- end
-
- it 'returns canonical_email_hash correctly' do
- post :create, params: params
-
- json = body_as_json
-
- expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
- end
-
- context 'when required email param is not provided' do
- it 'returns http unprocessable entity' do
- post :create
-
- expect(response).to have_http_status(422)
- end
- end
-
- context 'when canonical_email_hash param is provided instead of email' do
- let(:params) { { canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } }
-
- it 'returns http success' do
- post :create, params: params
-
- expect(response).to have_http_status(200)
- end
-
- it 'returns correct canonical_email_hash' do
- post :create, params: params
-
- json = body_as_json
-
- expect(json[:canonical_email_hash]).to eq(params[:canonical_email_hash])
- end
- end
-
- context 'when both email and canonical_email_hash params are provided' do
- let(:params) { { email: 'example@email.com', canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } }
-
- it 'returns http success' do
- post :create, params: params
-
- expect(response).to have_http_status(200)
- end
-
- it 'ignores canonical_email_hash param' do
- post :create, params: params
-
- json = body_as_json
-
- expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
- end
- end
-
- context 'when canonical email was already blocked' do
- before do
- canonical_email_block.save
- end
-
- it 'returns http unprocessable entity' do
- post :create, params: params
-
- expect(response).to have_http_status(422)
- end
- end
- end
-
- describe 'DELETE #destroy' do
- let!(:canonical_email_block) { Fabricate(:canonical_email_block) }
- let(:params) { { id: canonical_email_block.id } }
-
- context 'with wrong scope' do
- before do
- delete :destroy, params: params
- end
-
- it_behaves_like 'forbidden for wrong scope', 'read:statuses'
- end
-
- context 'with wrong role' do
- before do
- delete :destroy, params: params
- end
-
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
- end
-
- it 'returns http success' do
- delete :destroy, params: params
-
- expect(response).to have_http_status(200)
- end
-
- context 'when canonical email block is not found' do
- it 'returns http not found' do
- delete :destroy, params: { id: 0 }
-
- expect(response).to have_http_status(404)
- end
- end
- end
-end
diff --git a/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb b/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb
deleted file mode 100644
index 69aeb6451a..0000000000
--- a/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb
+++ /dev/null
@@ -1,140 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Api::V1::Admin::DomainAllowsController do
- render_views
-
- let(:role) { UserRole.find_by(name: 'Admin') }
- let(:user) { Fabricate(:user, role: role) }
- let(:scopes) { 'admin:read admin:write' }
- let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
-
- before do
- allow(controller).to receive(:doorkeeper_token) { token }
- end
-
- shared_examples 'forbidden for wrong scope' do |wrong_scope|
- let(:scopes) { wrong_scope }
-
- it 'returns http forbidden' do
- expect(response).to have_http_status(403)
- end
- end
-
- shared_examples 'forbidden for wrong role' do |wrong_role|
- let(:role) { UserRole.find_by(name: wrong_role) }
-
- it 'returns http forbidden' do
- expect(response).to have_http_status(403)
- end
- end
-
- describe 'GET #index' do
- let!(:domain_allow) { Fabricate(:domain_allow) }
-
- before do
- get :index
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
-
- it 'returns the expected domain allows' do
- json = body_as_json
- expect(json.length).to eq 1
- expect(json[0][:id].to_i).to eq domain_allow.id
- end
- end
-
- describe 'GET #show' do
- let!(:domain_allow) { Fabricate(:domain_allow) }
-
- before do
- get :show, params: { id: domain_allow.id }
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
-
- it 'returns expected domain name' do
- json = body_as_json
- expect(json[:domain]).to eq domain_allow.domain
- end
- end
-
- describe 'DELETE #destroy' do
- let!(:domain_allow) { Fabricate(:domain_allow) }
-
- before do
- delete :destroy, params: { id: domain_allow.id }
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
-
- it 'deletes the block' do
- expect(DomainAllow.find_by(id: domain_allow.id)).to be_nil
- end
- end
-
- describe 'POST #create' do
- let!(:domain_allow) { Fabricate(:domain_allow, domain: 'example.com') }
-
- context 'with a valid domain' do
- before do
- post :create, params: { domain: 'foo.bar.com' }
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
-
- it 'returns expected domain name' do
- json = body_as_json
- expect(json[:domain]).to eq 'foo.bar.com'
- end
-
- it 'creates a domain block' do
- expect(DomainAllow.find_by(domain: 'foo.bar.com')).to_not be_nil
- end
- end
-
- context 'with invalid domain name' do
- before do
- post :create, params: { domain: 'foo bar' }
- end
-
- it 'returns http unprocessable entity' do
- expect(response).to have_http_status(422)
- end
- end
-
- context 'when domain name is not specified' do
- it 'returns http unprocessable entity' do
- post :create
-
- expect(response).to have_http_status(422)
- end
- end
- end
-end
diff --git a/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb
deleted file mode 100644
index 5659843f7a..0000000000
--- a/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb
+++ /dev/null
@@ -1,180 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Api::V1::Admin::DomainBlocksController do
- render_views
-
- let(:role) { UserRole.find_by(name: 'Admin') }
- let(:user) { Fabricate(:user, role: role) }
- let(:scopes) { 'admin:read admin:write' }
- let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
-
- before do
- allow(controller).to receive(:doorkeeper_token) { token }
- end
-
- shared_examples 'forbidden for wrong scope' do |wrong_scope|
- let(:scopes) { wrong_scope }
-
- it 'returns http forbidden' do
- expect(response).to have_http_status(403)
- end
- end
-
- shared_examples 'forbidden for wrong role' do |wrong_role|
- let(:role) { UserRole.find_by(name: wrong_role) }
-
- it 'returns http forbidden' do
- expect(response).to have_http_status(403)
- end
- end
-
- describe 'GET #index' do
- let!(:block) { Fabricate(:domain_block) }
-
- before do
- get :index
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
-
- it 'returns the expected domain blocks' do
- json = body_as_json
- expect(json.length).to eq 1
- expect(json[0][:id].to_i).to eq block.id
- end
- end
-
- describe 'GET #show' do
- let!(:block) { Fabricate(:domain_block) }
-
- before do
- get :show, params: { id: block.id }
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
-
- it 'returns expected domain name' do
- json = body_as_json
- expect(json[:domain]).to eq block.domain
- end
- end
-
- describe 'PUT #update' do
- let!(:remote_account) { Fabricate(:account, domain: 'example.com') }
- let(:subject) do
- post :update, params: { id: domain_block.id, domain: 'example.com', severity: new_severity }
- end
- let(:domain_block) { Fabricate(:domain_block, domain: 'example.com', severity: original_severity) }
-
- before do
- BlockDomainService.new.call(domain_block)
- end
-
- context 'when downgrading a domain suspension to silence' do
- let(:original_severity) { 'suspend' }
- let(:new_severity) { 'silence' }
-
- it 'changes the block severity' do
- expect { subject }.to change { domain_block.reload.severity }.from('suspend').to('silence')
- end
-
- it 'undoes individual suspensions' do
- expect { subject }.to change { remote_account.reload.suspended? }.from(true).to(false)
- end
-
- it 'performs individual silences' do
- expect { subject }.to change { remote_account.reload.silenced? }.from(false).to(true)
- end
- end
-
- context 'when upgrading a domain silence to suspend' do
- let(:original_severity) { 'silence' }
- let(:new_severity) { 'suspend' }
-
- it 'changes the block severity' do
- expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend')
- end
-
- it 'undoes individual silences' do
- expect { subject }.to change { remote_account.reload.silenced? }.from(true).to(false)
- end
-
- it 'performs individual suspends' do
- expect { subject }.to change { remote_account.reload.suspended? }.from(false).to(true)
- end
- end
- end
-
- describe 'DELETE #destroy' do
- let!(:block) { Fabricate(:domain_block) }
-
- before do
- delete :destroy, params: { id: block.id }
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
-
- it 'deletes the block' do
- expect(DomainBlock.find_by(id: block.id)).to be_nil
- end
- end
-
- describe 'POST #create' do
- let(:existing_block_domain) { 'example.com' }
- let!(:block) { Fabricate(:domain_block, domain: existing_block_domain, severity: :suspend) }
-
- before do
- post :create, params: { domain: 'foo.bar.com', severity: :silence }
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
-
- it 'returns expected domain name' do
- json = body_as_json
- expect(json[:domain]).to eq 'foo.bar.com'
- end
-
- it 'creates a domain block' do
- expect(DomainBlock.find_by(domain: 'foo.bar.com')).to_not be_nil
- end
-
- context 'when a stricter domain block already exists' do
- let(:existing_block_domain) { 'bar.com' }
-
- it 'returns http unprocessable entity' do
- expect(response).to have_http_status(422)
- end
-
- it 'renders existing domain block in error' do
- json = body_as_json
- expect(json[:existing_domain_block][:domain]).to eq existing_block_domain
- end
- end
- end
-end
diff --git a/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb
deleted file mode 100644
index a5787883ee..0000000000
--- a/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb
+++ /dev/null
@@ -1,309 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Api::V1::Admin::IpBlocksController do
- render_views
-
- let(:role) { UserRole.find_by(name: 'Admin') }
- let(:user) { Fabricate(:user, role: role) }
- let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
- let(:scopes) { 'admin:read:ip_blocks admin:write:ip_blocks' }
-
- before do
- allow(controller).to receive(:doorkeeper_token) { token }
- end
-
- shared_examples 'forbidden for wrong scope' do |wrong_scope|
- let(:scopes) { wrong_scope }
-
- it 'returns http forbidden' do
- expect(response).to have_http_status(403)
- end
- end
-
- shared_examples 'forbidden for wrong role' do |wrong_role|
- let(:role) { UserRole.find_by(name: wrong_role) }
-
- it 'returns http forbidden' do
- expect(response).to have_http_status(403)
- end
- end
-
- describe 'GET #index' do
- context 'with wrong scope' do
- before do
- get :index
- end
-
- it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks'
- end
-
- context 'with wrong role' do
- before do
- get :index
- end
-
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
- end
-
- it 'returns http success' do
- get :index
-
- expect(response).to have_http_status(200)
- end
-
- context 'when there is no ip block' do
- it 'returns an empty body' do
- get :index
-
- json = body_as_json
-
- expect(json).to be_empty
- end
- end
-
- context 'when there are ip blocks' do
- let!(:ip_blocks) do
- [
- IpBlock.create(ip: '192.0.2.0/24', severity: :no_access),
- IpBlock.create(ip: '172.16.0.1', severity: :sign_up_requires_approval, comment: 'Spam'),
- IpBlock.create(ip: '2001:0db8::/32', severity: :sign_up_block, expires_in: 10.days),
- ]
- end
- let(:expected_response) do
- ip_blocks.map do |ip_block|
- {
- id: ip_block.id.to_s,
- ip: ip_block.ip,
- severity: ip_block.severity.to_s,
- comment: ip_block.comment,
- created_at: ip_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
- expires_at: ip_block.expires_at&.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
- }
- end
- end
-
- it 'returns the correct blocked ips' do
- get :index
-
- json = body_as_json
-
- expect(json).to match_array(expected_response)
- end
-
- context 'with limit param' do
- let(:params) { { limit: 2 } }
-
- it 'returns only the requested number of ip blocks' do
- get :index, params: params
-
- json = body_as_json
-
- expect(json.size).to eq(params[:limit])
- end
- end
- end
- end
-
- describe 'GET #show' do
- let!(:ip_block) { IpBlock.create(ip: '192.0.2.0/24', severity: :no_access) }
- let(:params) { { id: ip_block.id } }
-
- context 'with wrong scope' do
- before do
- get :show, params: params
- end
-
- it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks'
- end
-
- context 'with wrong role' do
- before do
- get :show, params: params
- end
-
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
- end
-
- it 'returns http success' do
- get :show, params: params
-
- expect(response).to have_http_status(200)
- end
-
- it 'returns the correct ip block' do
- get :show, params: params
-
- json = body_as_json
-
- expect(json[:ip]).to eq("#{ip_block.ip}/#{ip_block.ip.prefix}")
- expect(json[:severity]).to eq(ip_block.severity.to_s)
- end
-
- context 'when ip block does not exist' do
- it 'returns http not found' do
- get :show, params: { id: 0 }
-
- expect(response).to have_http_status(404)
- end
- end
- end
-
- describe 'POST #create' do
- let(:params) { { ip: '151.0.32.55', severity: 'no_access', comment: 'Spam' } }
-
- context 'with wrong scope' do
- before do
- post :create, params: params
- end
-
- it_behaves_like 'forbidden for wrong scope', 'admin:read:ip_blocks'
- end
-
- context 'with wrong role' do
- before do
- post :create, params: params
- end
-
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
- end
-
- it 'returns http success' do
- post :create, params: params
-
- expect(response).to have_http_status(200)
- end
-
- it 'returns the correct ip block' do
- post :create, params: params
-
- json = body_as_json
-
- expect(json[:ip]).to eq("#{params[:ip]}/32")
- expect(json[:severity]).to eq(params[:severity])
- expect(json[:comment]).to eq(params[:comment])
- end
-
- context 'when ip is not provided' do
- let(:params) { { ip: '', severity: 'no_access' } }
-
- it 'returns http unprocessable entity' do
- post :create, params: params
-
- expect(response).to have_http_status(422)
- end
- end
-
- context 'when severity is not provided' do
- let(:params) { { ip: '173.65.23.1', severity: '' } }
-
- it 'returns http unprocessable entity' do
- post :create, params: params
-
- expect(response).to have_http_status(422)
- end
- end
-
- context 'when provided ip is already blocked' do
- before do
- IpBlock.create(params)
- end
-
- it 'returns http unprocessable entity' do
- post :create, params: params
-
- expect(response).to have_http_status(422)
- end
- end
-
- context 'when provided ip address is invalid' do
- let(:params) { { ip: '520.13.54.120', severity: 'no_access' } }
-
- it 'returns http unprocessable entity' do
- post :create, params: params
-
- expect(response).to have_http_status(422)
- end
- end
- end
-
- describe 'PUT #update' do
- context 'when ip block exists' do
- let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access', comment: 'Spam', expires_in: 48.hours) }
- let(:params) { { id: ip_block.id, severity: 'sign_up_requires_approval', comment: 'Decreasing severity' } }
-
- it 'returns http success' do
- put :update, params: params
-
- expect(response).to have_http_status(200)
- end
-
- it 'returns the correct ip block' do
- put :update, params: params
-
- json = body_as_json
-
- expect(json).to match(hash_including({
- ip: "#{ip_block.ip}/#{ip_block.ip.prefix}",
- severity: 'sign_up_requires_approval',
- comment: 'Decreasing severity',
- }))
- end
-
- it 'updates the severity correctly' do
- expect { put :update, params: params }.to change { ip_block.reload.severity }.from('no_access').to('sign_up_requires_approval')
- end
-
- it 'updates the comment correctly' do
- expect { put :update, params: params }.to change { ip_block.reload.comment }.from('Spam').to('Decreasing severity')
- end
- end
-
- context 'when ip block does not exist' do
- it 'returns http not found' do
- put :update, params: { id: 0 }
-
- expect(response).to have_http_status(404)
- end
- end
- end
-
- describe 'DELETE #destroy' do
- context 'when ip block exists' do
- let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access') }
- let(:params) { { id: ip_block.id } }
-
- it 'returns http success' do
- delete :destroy, params: params
-
- expect(response).to have_http_status(200)
- end
-
- it 'returns an empty body' do
- delete :destroy, params: params
-
- json = body_as_json
-
- expect(json).to be_empty
- end
-
- it 'deletes the ip block' do
- delete :destroy, params: params
-
- expect(IpBlock.find_by(id: ip_block.id)).to be_nil
- end
- end
-
- context 'when ip block does not exist' do
- it 'returns http not found' do
- delete :destroy, params: { id: 0 }
-
- expect(response).to have_http_status(404)
- end
- end
- end
-end
diff --git a/spec/controllers/api/v1/admin/reports_controller_spec.rb b/spec/controllers/api/v1/admin/reports_controller_spec.rb
deleted file mode 100644
index 4f0c484e59..0000000000
--- a/spec/controllers/api/v1/admin/reports_controller_spec.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Api::V1::Admin::ReportsController do
- render_views
-
- let(:role) { UserRole.find_by(name: 'Moderator') }
- let(:user) { Fabricate(:user, role: role) }
- let(:scopes) { 'admin:read admin:write' }
- let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
- let(:report) { Fabricate(:report) }
-
- before do
- allow(controller).to receive(:doorkeeper_token) { token }
- end
-
- shared_examples 'forbidden for wrong scope' do |wrong_scope|
- let(:scopes) { wrong_scope }
-
- it 'returns http forbidden' do
- expect(response).to have_http_status(403)
- end
- end
-
- shared_examples 'forbidden for wrong role' do |wrong_role|
- let(:role) { UserRole.find_by(name: wrong_role) }
-
- it 'returns http forbidden' do
- expect(response).to have_http_status(403)
- end
- end
-
- describe 'GET #index' do
- before do
- get :index
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
- end
-
- describe 'GET #show' do
- before do
- get :show, params: { id: report.id }
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
- end
-
- describe 'POST #resolve' do
- before do
- post :resolve, params: { id: report.id }
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
- end
-
- describe 'POST #reopen' do
- before do
- post :reopen, params: { id: report.id }
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
- end
-
- describe 'POST #assign_to_self' do
- before do
- post :assign_to_self, params: { id: report.id }
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
- end
-
- describe 'POST #unassign' do
- before do
- post :unassign, params: { id: report.id }
- end
-
- it_behaves_like 'forbidden for wrong scope', 'write:statuses'
- it_behaves_like 'forbidden for wrong role', ''
-
- it 'returns http success' do
- expect(response).to have_http_status(200)
- end
- end
-end
diff --git a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
index 0b807b280f..84dfd60b38 100644
--- a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
@@ -56,18 +56,11 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
end
describe 'when creation succeeds' do
+ let!(:otp_backup_codes) { user.generate_otp_backup_codes! }
+
it 'renders page with success' do
- otp_backup_codes = user.generate_otp_backup_codes!
- expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value|
- expect(value).to eq user
- otp_backup_codes
- end
- expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, code, options|
- expect(value).to eq user
- expect(code).to eq '123456'
- expect(options).to eq({ otp_secret: 'thisisasecretforthespecofnewview' })
- true
- end
+ prepare_user_otp_generation
+ prepare_user_otp_consumption
expect do
post :create,
@@ -80,6 +73,22 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
expect(response).to have_http_status(200)
expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
end
+
+ def prepare_user_otp_generation
+ expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value|
+ expect(value).to eq user
+ otp_backup_codes
+ end
+ end
+
+ def prepare_user_otp_consumption
+ expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, code, options|
+ expect(value).to eq user
+ expect(code).to eq '123456'
+ expect(options).to eq({ otp_secret: 'thisisasecretforthespecofnewview' })
+ true
+ end
+ end
end
describe 'when creation fails' do
diff --git a/spec/fabricators/account_domain_block_fabricator.rb b/spec/fabricators/account_domain_block_fabricator.rb
index ff85e17f3f..83df509da2 100644
--- a/spec/fabricators/account_domain_block_fabricator.rb
+++ b/spec/fabricators/account_domain_block_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:account_domain_block) do
- account
+ account { Fabricate.build(:account) }
domain 'example.com'
end
diff --git a/spec/fabricators/account_moderation_note_fabricator.rb b/spec/fabricators/account_moderation_note_fabricator.rb
index 341a24dea0..05a687bf4e 100644
--- a/spec/fabricators/account_moderation_note_fabricator.rb
+++ b/spec/fabricators/account_moderation_note_fabricator.rb
@@ -2,6 +2,6 @@
Fabricator(:account_moderation_note) do
content 'MyText'
- account
- target_account { Fabricate(:account) }
+ account { Fabricate.build(:account) }
+ target_account { Fabricate.build(:account) }
end
diff --git a/spec/fabricators/account_note_fabricator.rb b/spec/fabricators/account_note_fabricator.rb
index bb4ed8b24d..241362c144 100644
--- a/spec/fabricators/account_note_fabricator.rb
+++ b/spec/fabricators/account_note_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:account_note) do
- account
- target_account { Fabricate(:account) }
+ account { Fabricate.build(:account) }
+ target_account { Fabricate.build(:account) }
comment 'User note text'
end
diff --git a/spec/fabricators/account_stat_fabricator.rb b/spec/fabricators/account_stat_fabricator.rb
index e6085c5f2b..20272fb22f 100644
--- a/spec/fabricators/account_stat_fabricator.rb
+++ b/spec/fabricators/account_stat_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:account_stat) do
- account
+ account { Fabricate.build(:account) }
statuses_count '123'
following_count '456'
followers_count '789'
diff --git a/spec/fabricators/account_statuses_cleanup_policy_fabricator.rb b/spec/fabricators/account_statuses_cleanup_policy_fabricator.rb
index 0e756ddbaa..fcf7a53475 100644
--- a/spec/fabricators/account_statuses_cleanup_policy_fabricator.rb
+++ b/spec/fabricators/account_statuses_cleanup_policy_fabricator.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
Fabricator(:account_statuses_cleanup_policy) do
- account
+ account { Fabricate.build(:account) }
end
diff --git a/spec/fabricators/account_warning_fabricator.rb b/spec/fabricators/account_warning_fabricator.rb
index e5059e37f5..70005a927b 100644
--- a/spec/fabricators/account_warning_fabricator.rb
+++ b/spec/fabricators/account_warning_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:account_warning) do
- account
+ account { Fabricate.build(:account) }
target_account(fabricator: :account)
text { Faker::Lorem.paragraph }
action 'suspend'
diff --git a/spec/fabricators/admin_action_log_fabricator.rb b/spec/fabricators/admin_action_log_fabricator.rb
index a259644bdc..3acedbffd3 100644
--- a/spec/fabricators/admin_action_log_fabricator.rb
+++ b/spec/fabricators/admin_action_log_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator('Admin::ActionLog') do
- account
+ account { Fabricate.build(:account) }
action 'MyString'
target nil
end
diff --git a/spec/fabricators/backup_fabricator.rb b/spec/fabricators/backup_fabricator.rb
index c73ae54bed..58e37c9875 100644
--- a/spec/fabricators/backup_fabricator.rb
+++ b/spec/fabricators/backup_fabricator.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
Fabricator(:backup) do
- user
+ user { Fabricate.build(:user) }
end
diff --git a/spec/fabricators/block_fabricator.rb b/spec/fabricators/block_fabricator.rb
index c2e9e9628d..c4087e46d2 100644
--- a/spec/fabricators/block_fabricator.rb
+++ b/spec/fabricators/block_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:block) do
- account
- target_account { Fabricate(:account) }
+ account { Fabricate.build(:account) }
+ target_account { Fabricate.build(:account) }
end
diff --git a/spec/fabricators/bookmark_fabricator.rb b/spec/fabricators/bookmark_fabricator.rb
index e21046fc25..994ac6e687 100644
--- a/spec/fabricators/bookmark_fabricator.rb
+++ b/spec/fabricators/bookmark_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:bookmark) do
- account
- status
+ account { Fabricate.build(:account) }
+ status { Fabricate.build(:status) }
end
diff --git a/spec/fabricators/bulk_import_fabricator.rb b/spec/fabricators/bulk_import_fabricator.rb
index 673b7960d9..d30758dfe0 100644
--- a/spec/fabricators/bulk_import_fabricator.rb
+++ b/spec/fabricators/bulk_import_fabricator.rb
@@ -8,5 +8,5 @@ Fabricator(:bulk_import) do
imported_items 1
finished_at '2022-11-18 14:55:07'
overwrite false
- account
+ account { Fabricate.build(:account) }
end
diff --git a/spec/fabricators/bulk_import_row_fabricator.rb b/spec/fabricators/bulk_import_row_fabricator.rb
index f8358e734d..10a4bf1608 100644
--- a/spec/fabricators/bulk_import_row_fabricator.rb
+++ b/spec/fabricators/bulk_import_row_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:bulk_import_row) do
- bulk_import
+ bulk_import { Fabricate.build(:bulk_import) }
data ''
end
diff --git a/spec/fabricators/canonical_email_block_fabricator.rb b/spec/fabricators/canonical_email_block_fabricator.rb
index 3a018059fc..1ef53ff4a4 100644
--- a/spec/fabricators/canonical_email_block_fabricator.rb
+++ b/spec/fabricators/canonical_email_block_fabricator.rb
@@ -2,5 +2,5 @@
Fabricator(:canonical_email_block) do
email { sequence(:email) { |i| "#{i}#{Faker::Internet.email}" } }
- reference_account { Fabricate(:account) }
+ reference_account { Fabricate.build(:account) }
end
diff --git a/spec/fabricators/custom_filter_fabricator.rb b/spec/fabricators/custom_filter_fabricator.rb
index 5fee4f01af..766cc3b115 100644
--- a/spec/fabricators/custom_filter_fabricator.rb
+++ b/spec/fabricators/custom_filter_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:custom_filter) do
- account
+ account { Fabricate.build(:account) }
expires_at nil
phrase 'discourse'
context %w(home notifications)
diff --git a/spec/fabricators/custom_filter_keyword_fabricator.rb b/spec/fabricators/custom_filter_keyword_fabricator.rb
index f1fb440dc5..aa4bf84739 100644
--- a/spec/fabricators/custom_filter_keyword_fabricator.rb
+++ b/spec/fabricators/custom_filter_keyword_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:custom_filter_keyword) do
- custom_filter
+ custom_filter { Fabricate.build(:custom_filter) }
keyword 'discourse'
end
diff --git a/spec/fabricators/custom_filter_status_fabricator.rb b/spec/fabricators/custom_filter_status_fabricator.rb
index 3ef1d0ec83..f66f62e561 100644
--- a/spec/fabricators/custom_filter_status_fabricator.rb
+++ b/spec/fabricators/custom_filter_status_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:custom_filter_status) do
- custom_filter
- status
+ custom_filter { Fabricate.build(:custom_filter) }
+ status { Fabricate.build(:status) }
end
diff --git a/spec/fabricators/device_fabricator.rb b/spec/fabricators/device_fabricator.rb
index 26c71b4fdd..37a2e8977d 100644
--- a/spec/fabricators/device_fabricator.rb
+++ b/spec/fabricators/device_fabricator.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
Fabricator(:device) do
- access_token
- account
+ access_token { Fabricate.build(:access_token) }
+ account { Fabricate.build(:account) }
device_id { Faker::Number.number(digits: 5) }
name { Faker::App.name }
fingerprint_key { Base64.strict_encode64(Ed25519::SigningKey.generate.verify_key.to_bytes) }
diff --git a/spec/fabricators/domain_allow_fabricator.rb b/spec/fabricators/domain_allow_fabricator.rb
index b32af129bc..12fdaaea1a 100644
--- a/spec/fabricators/domain_allow_fabricator.rb
+++ b/spec/fabricators/domain_allow_fabricator.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
Fabricator(:domain_allow) do
- domain 'MyString'
+ domain { sequence(:domain) { |i| "example#{i}.com" } }
end
diff --git a/spec/fabricators/encrypted_message_fabricator.rb b/spec/fabricators/encrypted_message_fabricator.rb
index 43b3105146..349b659c2f 100644
--- a/spec/fabricators/encrypted_message_fabricator.rb
+++ b/spec/fabricators/encrypted_message_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:encrypted_message) do
- device
- from_account { Fabricate(:account) }
+ device { Fabricate.build(:device) }
+ from_account { Fabricate.build(:account) }
from_device_id { Faker::Number.number(digits: 5) }
end
diff --git a/spec/fabricators/favourite_fabricator.rb b/spec/fabricators/favourite_fabricator.rb
index 005947e6f8..639416987e 100644
--- a/spec/fabricators/favourite_fabricator.rb
+++ b/spec/fabricators/favourite_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:favourite) do
- account
- status
+ account { Fabricate.build(:account) }
+ status { Fabricate.build(:status) }
end
diff --git a/spec/fabricators/featured_tag_fabricator.rb b/spec/fabricators/featured_tag_fabricator.rb
index 838364056b..0803dc43a7 100644
--- a/spec/fabricators/featured_tag_fabricator.rb
+++ b/spec/fabricators/featured_tag_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:featured_tag) do
- account
- tag
+ account { Fabricate.build(:account) }
+ tag { Fabricate.build(:tag) }
name { sequence(:name) { |i| "Tag#{i}" } }
end
diff --git a/spec/fabricators/follow_fabricator.rb b/spec/fabricators/follow_fabricator.rb
index 41b5305d55..29886b4301 100644
--- a/spec/fabricators/follow_fabricator.rb
+++ b/spec/fabricators/follow_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:follow) do
- account
- target_account { Fabricate(:account) }
+ account { Fabricate.build(:account) }
+ target_account { Fabricate.build(:account) }
end
diff --git a/spec/fabricators/follow_request_fabricator.rb b/spec/fabricators/follow_request_fabricator.rb
index 86b82611f7..6b2d658a37 100644
--- a/spec/fabricators/follow_request_fabricator.rb
+++ b/spec/fabricators/follow_request_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:follow_request) do
- account
- target_account { Fabricate(:account, locked: true) }
+ account { Fabricate.build(:account) }
+ target_account { Fabricate.build(:account, locked: true) }
end
diff --git a/spec/fabricators/identity_fabricator.rb b/spec/fabricators/identity_fabricator.rb
index 58072c0d65..83655ee839 100644
--- a/spec/fabricators/identity_fabricator.rb
+++ b/spec/fabricators/identity_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:identity) do
- user
+ user { Fabricate.build(:user) }
provider 'MyString'
uid 'MyString'
end
diff --git a/spec/fabricators/invite_fabricator.rb b/spec/fabricators/invite_fabricator.rb
index 4f47d6ce2f..8fdf5f9185 100644
--- a/spec/fabricators/invite_fabricator.rb
+++ b/spec/fabricators/invite_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:invite) do
- user
+ user { Fabricate.build(:user) }
expires_at nil
max_uses nil
uses 0
diff --git a/spec/fabricators/list_fabricator.rb b/spec/fabricators/list_fabricator.rb
index 47af752b8c..d2bdc10129 100644
--- a/spec/fabricators/list_fabricator.rb
+++ b/spec/fabricators/list_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:list) do
- account
+ account { Fabricate.build(:account) }
title 'MyString'
end
diff --git a/spec/fabricators/login_activity_fabricator.rb b/spec/fabricators/login_activity_fabricator.rb
index 2b30658ff5..3309a303db 100644
--- a/spec/fabricators/login_activity_fabricator.rb
+++ b/spec/fabricators/login_activity_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:login_activity) do
- user
+ user { Fabricate.build(:user) }
authentication_method 'password'
success true
failure_reason nil
diff --git a/spec/fabricators/marker_fabricator.rb b/spec/fabricators/marker_fabricator.rb
index 561c2553ae..641db6b9ec 100644
--- a/spec/fabricators/marker_fabricator.rb
+++ b/spec/fabricators/marker_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:marker) do
- user
+ user { Fabricate.build(:user) }
timeline 'home'
last_read_id 0
lock_version 0
diff --git a/spec/fabricators/media_attachment_fabricator.rb b/spec/fabricators/media_attachment_fabricator.rb
index 4a081dccbe..062d3cbfec 100644
--- a/spec/fabricators/media_attachment_fabricator.rb
+++ b/spec/fabricators/media_attachment_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:media_attachment) do
- account
+ account { Fabricate.build(:account) }
file do |attrs|
case attrs[:type]
diff --git a/spec/fabricators/mention_fabricator.rb b/spec/fabricators/mention_fabricator.rb
index 5a83928275..ee8160aeb3 100644
--- a/spec/fabricators/mention_fabricator.rb
+++ b/spec/fabricators/mention_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:mention) do
- account
- status
+ account { Fabricate.build(:account) }
+ status { Fabricate.build(:status) }
end
diff --git a/spec/fabricators/mute_fabricator.rb b/spec/fabricators/mute_fabricator.rb
index 242ae2b08e..a70d3ff26d 100644
--- a/spec/fabricators/mute_fabricator.rb
+++ b/spec/fabricators/mute_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:mute) do
- account
- target_account { Fabricate(:account) }
+ account { Fabricate.build(:account) }
+ target_account { Fabricate.build(:account) }
end
diff --git a/spec/fabricators/notification_fabricator.rb b/spec/fabricators/notification_fabricator.rb
index 1e0c809874..fdfd7673ef 100644
--- a/spec/fabricators/notification_fabricator.rb
+++ b/spec/fabricators/notification_fabricator.rb
@@ -2,5 +2,5 @@
Fabricator(:notification) do
activity fabricator: :status
- account
+ account { Fabricate.build(:account) }
end
diff --git a/spec/fabricators/one_time_key_fabricator.rb b/spec/fabricators/one_time_key_fabricator.rb
index cfb365cabb..505282e05d 100644
--- a/spec/fabricators/one_time_key_fabricator.rb
+++ b/spec/fabricators/one_time_key_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:one_time_key) do
- device
+ device { Fabricate.build(:device) }
key_id { Faker::Alphanumeric.alphanumeric(number: 10) }
key { Base64.strict_encode64(Ed25519::SigningKey.generate.verify_key.to_bytes) }
diff --git a/spec/fabricators/poll_fabricator.rb b/spec/fabricators/poll_fabricator.rb
index 19c3b1d164..0203609ce7 100644
--- a/spec/fabricators/poll_fabricator.rb
+++ b/spec/fabricators/poll_fabricator.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
Fabricator(:poll) do
- account
- status
+ account { Fabricate.build(:account) }
+ status { Fabricate.build(:status) }
expires_at { 7.days.from_now }
options %w(Foo Bar)
multiple false
diff --git a/spec/fabricators/poll_vote_fabricator.rb b/spec/fabricators/poll_vote_fabricator.rb
index 9099ae96fe..47813cdb71 100644
--- a/spec/fabricators/poll_vote_fabricator.rb
+++ b/spec/fabricators/poll_vote_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:poll_vote) do
- account
+ account { Fabricate.build(:account) }
poll
choice 0
end
diff --git a/spec/fabricators/report_fabricator.rb b/spec/fabricators/report_fabricator.rb
index 7124773ad0..ed890230a7 100644
--- a/spec/fabricators/report_fabricator.rb
+++ b/spec/fabricators/report_fabricator.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
Fabricator(:report) do
- account
- target_account { Fabricate(:account) }
+ account { Fabricate.build(:account) }
+ target_account { Fabricate.build(:account) }
comment 'You nasty'
action_taken_at nil
end
diff --git a/spec/fabricators/report_note_fabricator.rb b/spec/fabricators/report_note_fabricator.rb
index f257fe2b7d..080fad51ac 100644
--- a/spec/fabricators/report_note_fabricator.rb
+++ b/spec/fabricators/report_note_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:report_note) do
- report
- account { Fabricate(:account) }
+ report { Fabricate.build(:report) }
+ account { Fabricate.build(:account) }
content 'Test Content'
end
diff --git a/spec/fabricators/scheduled_status_fabricator.rb b/spec/fabricators/scheduled_status_fabricator.rb
index e517f258a2..eed275ab92 100644
--- a/spec/fabricators/scheduled_status_fabricator.rb
+++ b/spec/fabricators/scheduled_status_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:scheduled_status) do
- account
+ account { Fabricate.build(:account) }
scheduled_at { 20.hours.from_now }
end
diff --git a/spec/fabricators/session_activation_fabricator.rb b/spec/fabricators/session_activation_fabricator.rb
index b28d5e41d7..4b5244cec6 100644
--- a/spec/fabricators/session_activation_fabricator.rb
+++ b/spec/fabricators/session_activation_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:session_activation) do
- user
+ user { Fabricate.build(:user) }
session_id 'MyString'
end
diff --git a/spec/fabricators/status_fabricator.rb b/spec/fabricators/status_fabricator.rb
index 17ac9ccd8a..32a2cbf6ac 100644
--- a/spec/fabricators/status_fabricator.rb
+++ b/spec/fabricators/status_fabricator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
Fabricator(:status) do
- account
+ account { Fabricate.build(:account) }
text 'Lorem ipsum dolor sit amet'
after_build do |status|
diff --git a/spec/fabricators/status_pin_fabricator.rb b/spec/fabricators/status_pin_fabricator.rb
index 9ad0ac9de3..ceaaa34a79 100644
--- a/spec/fabricators/status_pin_fabricator.rb
+++ b/spec/fabricators/status_pin_fabricator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
Fabricator(:status_pin) do
- account
- status { |attrs| Fabricate(:status, account: attrs[:account], visibility: :public) }
+ account { Fabricate.build(:account) }
+ status { |attrs| Fabricate.build(:status, account: attrs[:account], visibility: :public) }
end
diff --git a/spec/fabricators/status_stat_fabricator.rb b/spec/fabricators/status_stat_fabricator.rb
new file mode 100644
index 0000000000..715e6d4ab2
--- /dev/null
+++ b/spec/fabricators/status_stat_fabricator.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+Fabricator(:status_stat) do
+ status
+ replies_count '123'
+ reblogs_count '456'
+ favourites_count '789'
+end
diff --git a/spec/fabricators/tag_follow_fabricator.rb b/spec/fabricators/tag_follow_fabricator.rb
index cbe5b09898..014435d606 100644
--- a/spec/fabricators/tag_follow_fabricator.rb
+++ b/spec/fabricators/tag_follow_fabricator.rb
@@ -2,5 +2,5 @@
Fabricator(:tag_follow) do
tag
- account
+ account { Fabricate.build(:account) }
end
diff --git a/spec/lib/mastodon/cli/cache_spec.rb b/spec/lib/mastodon/cli/cache_spec.rb
index f101bc200f..3ab42dc8ce 100644
--- a/spec/lib/mastodon/cli/cache_spec.rb
+++ b/spec/lib/mastodon/cli/cache_spec.rb
@@ -4,9 +4,68 @@ require 'rails_helper'
require 'mastodon/cli/cache'
describe Mastodon::CLI::Cache do
+ let(:cli) { described_class.new }
+
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
end
end
+
+ describe '#clear' do
+ before { allow(Rails.cache).to receive(:clear) }
+
+ it 'clears the Rails cache' do
+ expect { cli.invoke(:clear) }.to output(
+ a_string_including('OK')
+ ).to_stdout
+ expect(Rails.cache).to have_received(:clear)
+ end
+ end
+
+ describe '#recount' do
+ context 'with the `accounts` argument' do
+ let(:arguments) { ['accounts'] }
+ let(:account_stat) { Fabricate(:account_stat) }
+
+ before do
+ account_stat.update(statuses_count: 123)
+ end
+
+ it 're-calculates account records in the cache' do
+ expect { cli.invoke(:recount, arguments) }.to output(
+ a_string_including('OK')
+ ).to_stdout
+
+ expect(account_stat.reload.statuses_count).to be_zero
+ end
+ end
+
+ context 'with the `statuses` argument' do
+ let(:arguments) { ['statuses'] }
+ let(:status_stat) { Fabricate(:status_stat) }
+
+ before do
+ status_stat.update(replies_count: 123)
+ end
+
+ it 're-calculates account records in the cache' do
+ expect { cli.invoke(:recount, arguments) }.to output(
+ a_string_including('OK')
+ ).to_stdout
+
+ expect(status_stat.reload.replies_count).to be_zero
+ end
+ end
+
+ context 'with an unknown type' do
+ let(:arguments) { ['other-type'] }
+
+ it 'Exits with an error message' do
+ expect { cli.invoke(:recount, arguments) }.to output(
+ a_string_including('Unknown')
+ ).to_stdout.and raise_error(SystemExit)
+ end
+ end
+ end
end
diff --git a/spec/lib/mastodon/cli/feeds_spec.rb b/spec/lib/mastodon/cli/feeds_spec.rb
index 4e1e214eff..030f087212 100644
--- a/spec/lib/mastodon/cli/feeds_spec.rb
+++ b/spec/lib/mastodon/cli/feeds_spec.rb
@@ -4,9 +4,65 @@ require 'rails_helper'
require 'mastodon/cli/feeds'
describe Mastodon::CLI::Feeds do
+ let(:cli) { described_class.new }
+
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
end
end
+
+ describe '#build' do
+ before { Fabricate(:account) }
+
+ context 'with --all option' do
+ let(:options) { { all: true } }
+
+ it 'regenerates feeds for all accounts' do
+ expect { cli.invoke(:build, [], options) }.to output(
+ a_string_including('Regenerated feeds')
+ ).to_stdout
+ end
+ end
+
+ context 'with a username' do
+ before { Fabricate(:account, username: 'alice') }
+
+ let(:arguments) { ['alice'] }
+
+ it 'regenerates feeds for the account' do
+ expect { cli.invoke(:build, arguments) }.to output(
+ a_string_including('OK')
+ ).to_stdout
+ end
+ end
+
+ context 'with invalid username' do
+ let(:arguments) { ['invalid-username'] }
+
+ it 'displays an error and exits' do
+ expect { cli.invoke(:build, arguments) }.to output(
+ a_string_including('No such account')
+ ).to_stdout.and raise_error(SystemExit)
+ end
+ end
+ end
+
+ describe '#clear' do
+ before do
+ allow(redis).to receive(:del).with(key_namespace)
+ end
+
+ it 'clears the redis `feed:*` namespace' do
+ expect { cli.invoke(:clear) }.to output(
+ a_string_including('OK')
+ ).to_stdout
+
+ expect(redis).to have_received(:del).with(key_namespace).once
+ end
+
+ def key_namespace
+ redis.keys('feed:*')
+ end
+ end
end
diff --git a/spec/models/form/import_spec.rb b/spec/models/form/import_spec.rb
index 52cf1c96ed..2b70e396b9 100644
--- a/spec/models/form/import_spec.rb
+++ b/spec/models/form/import_spec.rb
@@ -245,17 +245,44 @@ RSpec.describe Form::Import do
expect(account.bulk_imports.first.rows.pluck(:data)).to match_array(expected_rows)
end
- it 'creates a BulkImport with expected attributes' do
- bulk_import = account.bulk_imports.first
- expect(bulk_import).to_not be_nil
- expect(bulk_import.type.to_sym).to eq subject.type.to_sym
- expect(bulk_import.original_filename).to eq subject.data.original_filename
- expect(bulk_import.likely_mismatched?).to eq subject.likely_mismatched?
- expect(bulk_import.overwrite?).to eq !!subject.overwrite # rubocop:disable Style/DoubleNegation
- expect(bulk_import.processed_items).to eq 0
- expect(bulk_import.imported_items).to eq 0
- expect(bulk_import.total_items).to eq bulk_import.rows.count
- expect(bulk_import.unconfirmed?).to be true
+ context 'with a BulkImport' do
+ let(:bulk_import) { account.bulk_imports.first }
+
+ it 'creates a non-nil bulk import' do
+ expect(bulk_import).to_not be_nil
+ end
+
+ it 'matches the subjects type' do
+ expect(bulk_import.type.to_sym).to eq subject.type.to_sym
+ end
+
+ it 'matches the subjects original filename' do
+ expect(bulk_import.original_filename).to eq subject.data.original_filename
+ end
+
+ it 'matches the subjects likely_mismatched? value' do
+ expect(bulk_import.likely_mismatched?).to eq subject.likely_mismatched?
+ end
+
+ it 'matches the subject overwrite value' do
+ expect(bulk_import.overwrite?).to eq !!subject.overwrite # rubocop:disable Style/DoubleNegation
+ end
+
+ it 'has zero processed items' do
+ expect(bulk_import.processed_items).to eq 0
+ end
+
+ it 'has zero imported items' do
+ expect(bulk_import.imported_items).to eq 0
+ end
+
+ it 'has a correct total_items value' do
+ expect(bulk_import.total_items).to eq bulk_import.rows.count
+ end
+
+ it 'defaults to unconfirmed true' do
+ expect(bulk_import.unconfirmed?).to be true
+ end
end
end
diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb
index 4d4bc748f7..2dfc6cf925 100644
--- a/spec/models/media_attachment_spec.rb
+++ b/spec/models/media_attachment_spec.rb
@@ -2,7 +2,7 @@
require 'rails_helper'
-RSpec.describe MediaAttachment do
+RSpec.describe MediaAttachment, paperclip_processing: true do
describe 'local?' do
subject { media_attachment.local? }
diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb
index 0dd9264e00..d6e2282022 100644
--- a/spec/models/notification_spec.rb
+++ b/spec/models/notification_spec.rb
@@ -99,73 +99,87 @@ RSpec.describe Notification do
]
end
- it 'preloads target status' do
- # mention
- expect(subject[0].type).to eq :mention
- expect(subject[0].association(:mention)).to be_loaded
- expect(subject[0].mention.association(:status)).to be_loaded
+ context 'with a preloaded target status' do
+ it 'preloads mention' do
+ expect(subject[0].type).to eq :mention
+ expect(subject[0].association(:mention)).to be_loaded
+ expect(subject[0].mention.association(:status)).to be_loaded
+ end
- # status
- expect(subject[1].type).to eq :status
- expect(subject[1].association(:status)).to be_loaded
+ it 'preloads status' do
+ expect(subject[1].type).to eq :status
+ expect(subject[1].association(:status)).to be_loaded
+ end
- # reblog
- expect(subject[2].type).to eq :reblog
- expect(subject[2].association(:status)).to be_loaded
- expect(subject[2].status.association(:reblog)).to be_loaded
+ it 'preloads reblog' do
+ expect(subject[2].type).to eq :reblog
+ expect(subject[2].association(:status)).to be_loaded
+ expect(subject[2].status.association(:reblog)).to be_loaded
+ end
- # follow: nothing
- expect(subject[3].type).to eq :follow
- expect(subject[3].target_status).to be_nil
+ it 'preloads follow as nil' do
+ expect(subject[3].type).to eq :follow
+ expect(subject[3].target_status).to be_nil
+ end
- # follow_request: nothing
- expect(subject[4].type).to eq :follow_request
- expect(subject[4].target_status).to be_nil
+ it 'preloads follow_request as nill' do
+ expect(subject[4].type).to eq :follow_request
+ expect(subject[4].target_status).to be_nil
+ end
- # favourite
- expect(subject[5].type).to eq :favourite
- expect(subject[5].association(:favourite)).to be_loaded
- expect(subject[5].favourite.association(:status)).to be_loaded
+ it 'preloads favourite' do
+ expect(subject[5].type).to eq :favourite
+ expect(subject[5].association(:favourite)).to be_loaded
+ expect(subject[5].favourite.association(:status)).to be_loaded
+ end
- # poll
- expect(subject[6].type).to eq :poll
- expect(subject[6].association(:poll)).to be_loaded
- expect(subject[6].poll.association(:status)).to be_loaded
+ it 'preloads poll' do
+ expect(subject[6].type).to eq :poll
+ expect(subject[6].association(:poll)).to be_loaded
+ expect(subject[6].poll.association(:status)).to be_loaded
+ end
end
- it 'replaces to cached status' do
- # mention
- expect(subject[0].type).to eq :mention
- expect(subject[0].target_status.association(:account)).to be_loaded
- expect(subject[0].target_status).to eq mention.status
+ context 'with a cached status' do
+ it 'replaces mention' do
+ expect(subject[0].type).to eq :mention
+ expect(subject[0].target_status.association(:account)).to be_loaded
+ expect(subject[0].target_status).to eq mention.status
+ end
- # status
- expect(subject[1].type).to eq :status
- expect(subject[1].target_status.association(:account)).to be_loaded
- expect(subject[1].target_status).to eq status
+ it 'replaces status' do
+ expect(subject[1].type).to eq :status
+ expect(subject[1].target_status.association(:account)).to be_loaded
+ expect(subject[1].target_status).to eq status
+ end
- # reblog
- expect(subject[2].type).to eq :reblog
- expect(subject[2].target_status.association(:account)).to be_loaded
- expect(subject[2].target_status).to eq reblog.reblog
+ it 'replaces reblog' do
+ expect(subject[2].type).to eq :reblog
+ expect(subject[2].target_status.association(:account)).to be_loaded
+ expect(subject[2].target_status).to eq reblog.reblog
+ end
- # follow: nothing
- expect(subject[3].type).to eq :follow
- expect(subject[3].target_status).to be_nil
+ it 'replaces follow' do
+ expect(subject[3].type).to eq :follow
+ expect(subject[3].target_status).to be_nil
+ end
- # follow_request: nothing
- expect(subject[4].type).to eq :follow_request
- expect(subject[4].target_status).to be_nil
+ it 'replaces follow_request' do
+ expect(subject[4].type).to eq :follow_request
+ expect(subject[4].target_status).to be_nil
+ end
- # favourite
- expect(subject[5].type).to eq :favourite
- expect(subject[5].target_status.association(:account)).to be_loaded
- expect(subject[5].target_status).to eq favourite.status
+ it 'replaces favourite' do
+ expect(subject[5].type).to eq :favourite
+ expect(subject[5].target_status.association(:account)).to be_loaded
+ expect(subject[5].target_status).to eq favourite.status
+ end
- # poll
- expect(subject[6].type).to eq :poll
- expect(subject[6].target_status.association(:account)).to be_loaded
- expect(subject[6].target_status).to eq poll.status
+ it 'replaces poll' do
+ expect(subject[6].type).to eq :poll
+ expect(subject[6].target_status.association(:account)).to be_loaded
+ expect(subject[6].target_status).to eq poll.status
+ end
end
end
end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index d7e2b5c185..2645f74e40 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -79,6 +79,7 @@ RSpec.configure do |config|
config.before :each, type: :cli do
stub_stdout
+ stub_reset_connection_pools
end
config.before :each, type: :feature do
@@ -94,6 +95,12 @@ RSpec.configure do |config|
stub_jsonld_contexts!
end
+ config.before(:each) do |example|
+ unless example.metadata[:paperclip_processing]
+ allow_any_instance_of(Paperclip::Attachment).to receive(:post_process).and_return(true) # rubocop:disable RSpec/AnyInstance
+ end
+ end
+
config.after :each do
Rails.cache.clear
redis.del(redis.keys)
@@ -115,9 +122,20 @@ def attachment_fixture(name)
end
def stub_stdout
+ # TODO: Is there a bettery way to:
+ # - Avoid CLI command output being printed out
+ # - Allow rspec to assert things against STDOUT
+ # - Avoid disabling stdout for other desirable output (deprecation warnings, for example)
allow($stdout).to receive(:write)
end
+def stub_reset_connection_pools
+ # TODO: Is there a better way to correctly run specs without stubbing this?
+ # (Avoids reset_connection_pools! in test env)
+ allow(ActiveRecord::Base).to receive(:establish_connection)
+ allow(RedisConfiguration).to receive(:establish_pool)
+end
+
def stub_jsonld_contexts!
stub_request(:get, 'https://www.w3.org/ns/activitystreams').to_return(request_fixture('json-ld.activitystreams.txt'))
stub_request(:get, 'https://w3id.org/identity/v1').to_return(request_fixture('json-ld.identity.txt'))
diff --git a/spec/requests/api/v1/accounts_show_spec.rb b/spec/requests/api/v1/accounts_show_spec.rb
new file mode 100644
index 0000000000..ee6e925aa9
--- /dev/null
+++ b/spec/requests/api/v1/accounts_show_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'GET /api/v1/accounts/{account_id}' do
+ it 'returns account entity as 200 OK' do
+ account = Fabricate(:account)
+
+ get "/api/v1/accounts/#{account.id}"
+
+ aggregate_failures do
+ expect(response).to have_http_status(200)
+ expect(body_as_json[:id]).to eq(account.id.to_s)
+ end
+ end
+
+ it 'returns 404 if account not found' do
+ get '/api/v1/accounts/1'
+
+ aggregate_failures do
+ expect(response).to have_http_status(404)
+ expect(body_as_json[:error]).to eq('Record not found')
+ end
+ end
+
+ context 'when with token' do
+ it 'returns account entity as 200 OK if token is valid' do
+ account = Fabricate(:account)
+ user = Fabricate(:user, account: account)
+ token = Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts').token
+
+ get "/api/v1/accounts/#{account.id}", headers: { Authorization: "Bearer #{token}" }
+
+ aggregate_failures do
+ expect(response).to have_http_status(200)
+ expect(body_as_json[:id]).to eq(account.id.to_s)
+ end
+ end
+
+ it 'returns 403 if scope of token is invalid' do
+ account = Fabricate(:account)
+ user = Fabricate(:user, account: account)
+ token = Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:statuses').token
+
+ get "/api/v1/accounts/#{account.id}", headers: { Authorization: "Bearer #{token}" }
+
+ aggregate_failures do
+ expect(response).to have_http_status(403)
+ expect(body_as_json[:error]).to eq('This action is outside the authorized scopes')
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v1/admin/canonical_email_blocks_spec.rb b/spec/requests/api/v1/admin/canonical_email_blocks_spec.rb
new file mode 100644
index 0000000000..d70e6fc8a1
--- /dev/null
+++ b/spec/requests/api/v1/admin/canonical_email_blocks_spec.rb
@@ -0,0 +1,305 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Canonical Email Blocks' do
+ let(:role) { UserRole.find_by(name: 'Admin') }
+ let(:user) { Fabricate(:user, role: role) }
+ let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+ let(:scopes) { 'admin:read:canonical_email_blocks admin:write:canonical_email_blocks' }
+ let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
+
+ shared_examples 'forbidden for wrong scope' do |wrong_scope|
+ let(:scopes) { wrong_scope }
+
+ it 'returns http forbidden' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ shared_examples 'forbidden for wrong role' do |wrong_role|
+ let(:role) { UserRole.find_by(name: wrong_role) }
+
+ it 'returns http forbidden' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'GET /api/v1/admin/canonical_email_blocks' do
+ subject do
+ get '/api/v1/admin/canonical_email_blocks', headers: headers, params: params
+ end
+
+ let(:params) { {} }
+
+ it_behaves_like 'forbidden for wrong scope', 'read:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when there is no canonical email block' do
+ it 'returns an empty list' do
+ subject
+
+ expect(body_as_json).to be_empty
+ end
+ end
+
+ context 'when there are canonical email blocks' do
+ let!(:canonical_email_blocks) { Fabricate.times(5, :canonical_email_block) }
+ let(:expected_email_hashes) { canonical_email_blocks.pluck(:canonical_email_hash) }
+
+ it 'returns the correct canonical email hashes' do
+ subject
+
+ expect(body_as_json.pluck(:canonical_email_hash)).to match_array(expected_email_hashes)
+ end
+
+ context 'with limit param' do
+ let(:params) { { limit: 2 } }
+
+ it 'returns only the requested number of canonical email blocks' do
+ subject
+
+ expect(body_as_json.size).to eq(params[:limit])
+ end
+ end
+
+ context 'with since_id param' do
+ let(:params) { { since_id: canonical_email_blocks[1].id } }
+
+ it 'returns only the canonical email blocks after since_id' do
+ subject
+
+ canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s)
+
+ expect(body_as_json.pluck(:id)).to match_array(canonical_email_blocks_ids[2..])
+ end
+ end
+
+ context 'with max_id param' do
+ let(:params) { { max_id: canonical_email_blocks[3].id } }
+
+ it 'returns only the canonical email blocks before max_id' do
+ subject
+
+ canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s)
+
+ expect(body_as_json.pluck(:id)).to match_array(canonical_email_blocks_ids[..2])
+ end
+ end
+ end
+ end
+
+ describe 'GET /api/v1/admin/canonical_email_blocks/:id' do
+ subject do
+ get "/api/v1/admin/canonical_email_blocks/#{canonical_email_block.id}", headers: headers
+ end
+
+ let!(:canonical_email_block) { Fabricate(:canonical_email_block) }
+
+ it_behaves_like 'forbidden for wrong scope', 'read:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ context 'when the requested canonical email block exists' do
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the requested canonical email block data correctly' do
+ subject
+
+ json = body_as_json
+
+ expect(json[:id]).to eq(canonical_email_block.id.to_s)
+ expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
+ end
+ end
+
+ context 'when the requested canonical block does not exist' do
+ it 'returns http not found' do
+ get '/api/v1/admin/canonical_email_blocks/-1', headers: headers
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /api/v1/admin/canonical_email_blocks/test' do
+ subject do
+ post '/api/v1/admin/canonical_email_blocks/test', headers: headers, params: params
+ end
+
+ let(:params) { { email: 'email@example.com' } }
+
+ it_behaves_like 'forbidden for wrong scope', 'read:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ context 'when the required email param is not provided' do
+ let(:params) { {} }
+
+ it 'returns http bad request' do
+ subject
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context 'when the required email param is provided' do
+ context 'when there is a matching canonical email block' do
+ let!(:canonical_email_block) { CanonicalEmailBlock.create(params) }
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the expected canonical email hash' do
+ subject
+
+ expect(body_as_json[0][:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
+ end
+ end
+
+ context 'when there is no matching canonical email block' do
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns an empty list' do
+ subject
+
+ expect(body_as_json).to be_empty
+ end
+ end
+ end
+ end
+
+ describe 'POST /api/v1/admin/canonical_email_blocks' do
+ subject do
+ post '/api/v1/admin/canonical_email_blocks', headers: headers, params: params
+ end
+
+ let(:params) { { email: 'example@email.com' } }
+ let(:canonical_email_block) { CanonicalEmailBlock.new(email: params[:email]) }
+
+ it_behaves_like 'forbidden for wrong scope', 'read:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the canonical_email_hash correctly' do
+ subject
+
+ expect(body_as_json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
+ end
+
+ context 'when the required email param is not provided' do
+ let(:params) { {} }
+
+ it 'returns http unprocessable entity' do
+ subject
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'when the canonical_email_hash param is provided instead of email' do
+ let(:params) { { canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } }
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the correct canonical_email_hash' do
+ subject
+
+ expect(body_as_json[:canonical_email_hash]).to eq(params[:canonical_email_hash])
+ end
+ end
+
+ context 'when both email and canonical_email_hash params are provided' do
+ let(:params) { { email: 'example@email.com', canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } }
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'ignores the canonical_email_hash param' do
+ subject
+
+ expect(body_as_json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
+ end
+ end
+
+ context 'when the given canonical email was already blocked' do
+ before do
+ canonical_email_block.save
+ end
+
+ it 'returns http unprocessable entity' do
+ subject
+
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ describe 'DELETE /api/v1/admin/canonical_email_blocks/:id' do
+ subject do
+ delete "/api/v1/admin/canonical_email_blocks/#{canonical_email_block.id}", headers: headers
+ end
+
+ let!(:canonical_email_block) { Fabricate(:canonical_email_block) }
+
+ it_behaves_like 'forbidden for wrong scope', 'read:statuses'
+
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'deletes the canonical email block' do
+ subject
+
+ expect(CanonicalEmailBlock.find_by(id: canonical_email_block.id)).to be_nil
+ end
+
+ context 'when the canonical email block is not found' do
+ it 'returns http not found' do
+ delete '/api/v1/admin/canonical_email_blocks/0', headers: headers
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v1/admin/domain_allows_spec.rb b/spec/requests/api/v1/admin/domain_allows_spec.rb
new file mode 100644
index 0000000000..eb7915e77a
--- /dev/null
+++ b/spec/requests/api/v1/admin/domain_allows_spec.rb
@@ -0,0 +1,214 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Domain Allows' do
+ let(:role) { UserRole.find_by(name: 'Admin') }
+ let(:user) { Fabricate(:user, role: role) }
+ let(:scopes) { 'admin:read admin:write' }
+ let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+ let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
+
+ shared_examples 'forbidden for wrong scope' do |wrong_scope|
+ let(:scopes) { wrong_scope }
+
+ it 'returns http forbidden' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ shared_examples 'forbidden for wrong role' do |wrong_role|
+ let(:role) { UserRole.find_by(name: wrong_role) }
+
+ it 'returns http forbidden' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'GET /api/v1/admin/domain_allows' do
+ subject do
+ get '/api/v1/admin/domain_allows', headers: headers, params: params
+ end
+
+ let(:params) { {} }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when there is no allowed domains' do
+ it 'returns an empty body' do
+ subject
+
+ expect(body_as_json).to be_empty
+ end
+ end
+
+ context 'when there are allowed domains' do
+ let!(:domain_allows) { Fabricate.times(5, :domain_allow) }
+ let(:expected_response) do
+ domain_allows.map do |domain_allow|
+ {
+ id: domain_allow.id.to_s,
+ domain: domain_allow.domain,
+ created_at: domain_allow.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
+ }
+ end
+ end
+
+ it 'returns the correct allowed domains' do
+ subject
+
+ expect(body_as_json).to match_array(expected_response)
+ end
+
+ context 'with limit param' do
+ let(:params) { { limit: 2 } }
+
+ it 'returns only the requested number of allowed domains' do
+ subject
+
+ expect(body_as_json.size).to eq(params[:limit])
+ end
+ end
+ end
+ end
+
+ describe 'GET /api/v1/admin/domain_allows/:id' do
+ subject do
+ get "/api/v1/admin/domain_allows/#{domain_allow.id}", headers: headers
+ end
+
+ let!(:domain_allow) { Fabricate(:domain_allow) }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the expected allowed domain name' do
+ subject
+
+ expect(body_as_json[:domain]).to eq domain_allow.domain
+ end
+
+ context 'when the requested allowed domain does not exist' do
+ it 'returns http not found' do
+ get '/api/v1/admin/domain_allows/-1', headers: headers
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /api/v1/admin/domain_allows' do
+ subject do
+ post '/api/v1/admin/domain_allows', headers: headers, params: params
+ end
+
+ let(:params) { { domain: 'foo.bar.com' } }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ context 'with a valid domain name' do
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the expected domain name' do
+ subject
+
+ expect(body_as_json[:domain]).to eq 'foo.bar.com'
+ end
+
+ it 'creates a domain allow' do
+ subject
+
+ expect(DomainAllow.find_by(domain: 'foo.bar.com')).to be_present
+ end
+ end
+
+ context 'with invalid domain name' do
+ let(:params) { 'foo bar' }
+
+ it 'returns http unprocessable entity' do
+ subject
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'when domain name is not specified' do
+ let(:params) { {} }
+
+ it 'returns http unprocessable entity' do
+ subject
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'when the domain is already allowed' do
+ before do
+ DomainAllow.create(params)
+ end
+
+ it 'returns the existing allowed domain name' do
+ subject
+
+ expect(body_as_json[:domain]).to eq(params[:domain])
+ end
+ end
+ end
+
+ describe 'DELETE /api/v1/admin/domain_allows/:id' do
+ subject do
+ delete "/api/v1/admin/domain_allows/#{domain_allow.id}", headers: headers
+ end
+
+ let!(:domain_allow) { Fabricate(:domain_allow) }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'deletes the allowed domain' do
+ subject
+
+ expect(DomainAllow.find_by(id: domain_allow.id)).to be_nil
+ end
+
+ context 'when the allowed domain does not exist' do
+ it 'returns http not found' do
+ delete '/api/v1/admin/domain_allows/-1', headers: headers
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v1/admin/domain_blocks_spec.rb b/spec/requests/api/v1/admin/domain_blocks_spec.rb
new file mode 100644
index 0000000000..b3d52311b3
--- /dev/null
+++ b/spec/requests/api/v1/admin/domain_blocks_spec.rb
@@ -0,0 +1,284 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Domain Blocks' do
+ let(:role) { UserRole.find_by(name: 'Admin') }
+ let(:user) { Fabricate(:user, role: role) }
+ let(:scopes) { 'admin:read:domain_blocks admin:write:domain_blocks' }
+ let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+ let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
+
+ shared_examples 'forbidden for wrong scope' do |wrong_scope|
+ let(:scopes) { wrong_scope }
+
+ it 'returns http forbidden' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ shared_examples 'forbidden for wrong role' do |wrong_role|
+ let(:role) { UserRole.find_by(name: wrong_role) }
+
+ it 'returns http forbidden' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'GET /api/v1/admin/domain_blocks' do
+ subject do
+ get '/api/v1/admin/domain_blocks', headers: headers, params: params
+ end
+
+ let(:params) { {} }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when there are no domain blocks' do
+ it 'returns an empty list' do
+ subject
+
+ expect(body_as_json).to be_empty
+ end
+ end
+
+ context 'when there are domain blocks' do
+ let!(:domain_blocks) do
+ [
+ Fabricate(:domain_block, severity: :silence, reject_media: true),
+ Fabricate(:domain_block, severity: :suspend, obfuscate: true),
+ Fabricate(:domain_block, severity: :noop, reject_reports: true),
+ Fabricate(:domain_block, public_comment: 'Spam'),
+ Fabricate(:domain_block, private_comment: 'Spam'),
+ ]
+ end
+ let(:expected_responde) do
+ domain_blocks.map do |domain_block|
+ {
+ id: domain_block.id.to_s,
+ domain: domain_block.domain,
+ created_at: domain_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
+ severity: domain_block.severity.to_s,
+ reject_media: domain_block.reject_media,
+ reject_reports: domain_block.reject_reports,
+ private_comment: domain_block.private_comment,
+ public_comment: domain_block.public_comment,
+ obfuscate: domain_block.obfuscate,
+ }
+ end
+ end
+
+ it 'returns the expected domain blocks' do
+ subject
+
+ expect(body_as_json).to match_array(expected_responde)
+ end
+
+ context 'with limit param' do
+ let(:params) { { limit: 2 } }
+
+ it 'returns only the requested number of domain blocks' do
+ subject
+
+ expect(body_as_json.size).to eq(params[:limit])
+ end
+ end
+ end
+ end
+
+ describe 'GET /api/v1/admin/domain_blocks/:id' do
+ subject do
+ get "/api/v1/admin/domain_blocks/#{domain_block.id}", headers: headers
+ end
+
+ let!(:domain_block) { Fabricate(:domain_block) }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the expected domain block content' do
+ subject
+
+ expect(body_as_json).to eq(
+ {
+ id: domain_block.id.to_s,
+ domain: domain_block.domain,
+ created_at: domain_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
+ severity: domain_block.severity.to_s,
+ reject_media: domain_block.reject_media,
+ reject_reports: domain_block.reject_reports,
+ private_comment: domain_block.private_comment,
+ public_comment: domain_block.public_comment,
+ obfuscate: domain_block.obfuscate,
+ }
+ )
+ end
+
+ context 'when the requested domain block does not exist' do
+ it 'returns http not found' do
+ get '/api/v1/admin/domain_blocks/-1', headers: headers
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /api/v1/admin/domain_blocks' do
+ subject do
+ post '/api/v1/admin/domain_blocks', headers: headers, params: params
+ end
+
+ let(:params) { { domain: 'foo.bar.com', severity: :silence } }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns expected domain name and severity' do
+ subject
+
+ body = body_as_json
+
+ expect(body).to match a_hash_including(
+ {
+ domain: 'foo.bar.com',
+ severity: 'silence',
+ }
+ )
+ end
+
+ it 'creates a domain block' do
+ subject
+
+ expect(DomainBlock.find_by(domain: 'foo.bar.com')).to be_present
+ end
+
+ context 'when a stricter domain block already exists' do
+ before do
+ Fabricate(:domain_block, domain: 'bar.com', severity: :suspend)
+ end
+
+ it 'returns http unprocessable entity' do
+ subject
+
+ expect(response).to have_http_status(422)
+ end
+
+ it 'returns existing domain block in error' do
+ subject
+
+ expect(body_as_json[:existing_domain_block][:domain]).to eq('bar.com')
+ end
+ end
+
+ context 'when given domain name is invalid' do
+ let(:params) { { domain: 'foo bar', severity: :silence } }
+
+ it 'returns http unprocessable entity' do
+ subject
+
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ describe 'PUT /api/v1/admin/domain_blocks/:id' do
+ subject do
+ put "/api/v1/admin/domain_blocks/#{domain_block.id}", headers: headers, params: params
+ end
+
+ let!(:domain_block) { Fabricate(:domain_block, domain: 'example.com', severity: :silence) }
+ let(:params) { { domain: 'example.com', severity: 'suspend' } }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the updated domain block' do
+ subject
+
+ expect(body_as_json).to match a_hash_including(
+ {
+ id: domain_block.id.to_s,
+ domain: domain_block.domain,
+ severity: 'suspend',
+ }
+ )
+ end
+
+ it 'updates the block severity' do
+ expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend')
+ end
+
+ context 'when domain block does not exist' do
+ it 'returns http not found' do
+ put '/api/v1/admin/domain_blocks/-1', headers: headers
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'DELETE /api/v1/admin/domain_blocks/:id' do
+ subject do
+ delete "/api/v1/admin/domain_blocks/#{domain_block.id}", headers: headers
+ end
+
+ let!(:domain_block) { Fabricate(:domain_block) }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'deletes the domain block' do
+ subject
+
+ expect(DomainBlock.find_by(id: domain_block.id)).to be_nil
+ end
+
+ context 'when domain block does not exist' do
+ it 'returns http not found' do
+ delete '/api/v1/admin/domain_blocks/-1', headers: headers
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb b/spec/requests/api/v1/admin/email_domain_blocks_spec.rb
similarity index 52%
rename from spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb
rename to spec/requests/api/v1/admin/email_domain_blocks_spec.rb
index 3643eb0f3d..a24f22be21 100644
--- a/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb
+++ b/spec/requests/api/v1/admin/email_domain_blocks_spec.rb
@@ -2,23 +2,20 @@
require 'rails_helper'
-describe Api::V1::Admin::EmailDomainBlocksController do
- render_views
-
+RSpec.describe 'Email Domain Blocks' do
let(:role) { UserRole.find_by(name: 'Admin') }
let(:user) { Fabricate(:user, role: role) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:account) { Fabricate(:account) }
let(:scopes) { 'admin:read:email_domain_blocks admin:write:email_domain_blocks' }
-
- before do
- allow(controller).to receive(:doorkeeper_token) { token }
- end
+ let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
+ subject
+
expect(response).to have_http_status(403)
end
end
@@ -27,65 +24,54 @@ describe Api::V1::Admin::EmailDomainBlocksController do
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
+ subject
+
expect(response).to have_http_status(403)
end
end
- describe 'GET #index' do
- context 'with wrong scope' do
- before do
- get :index
- end
-
- it_behaves_like 'forbidden for wrong scope', 'read:statuses'
+ describe 'GET /api/v1/admin/email_domain_blocks' do
+ subject do
+ get '/api/v1/admin/email_domain_blocks', headers: headers, params: params
end
- context 'with wrong role' do
- before do
- get :index
- end
+ let(:params) { {} }
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
- end
+ it_behaves_like 'forbidden for wrong scope', 'read:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
- get :index
+ subject
expect(response).to have_http_status(200)
end
context 'when there is no email domain block' do
it 'returns an empty list' do
- get :index
+ subject
- json = body_as_json
-
- expect(json).to be_empty
+ expect(body_as_json).to be_empty
end
end
context 'when there are email domain blocks' do
- let!(:email_domain_blocks) { Fabricate.times(5, :email_domain_block) }
+ let!(:email_domain_blocks) { Fabricate.times(5, :email_domain_block) }
let(:blocked_email_domains) { email_domain_blocks.pluck(:domain) }
it 'return the correct blocked email domains' do
- get :index
+ subject
- json = body_as_json
-
- expect(json.pluck(:domain)).to match_array(blocked_email_domains)
+ expect(body_as_json.pluck(:domain)).to match_array(blocked_email_domains)
end
context 'with limit param' do
let(:params) { { limit: 2 } }
it 'returns only the requested number of email domain blocks' do
- get :index, params: params
+ subject
- json = body_as_json
-
- expect(json.size).to eq(params[:limit])
+ expect(body_as_json.size).to eq(params[:limit])
end
end
@@ -93,12 +79,11 @@ describe Api::V1::Admin::EmailDomainBlocksController do
let(:params) { { since_id: email_domain_blocks[1].id } }
it 'returns only the email domain blocks after since_id' do
- get :index, params: params
+ subject
email_domain_blocks_ids = email_domain_blocks.pluck(:id).map(&:to_s)
- json = body_as_json
- expect(json.pluck(:id)).to match_array(email_domain_blocks_ids[2..])
+ expect(body_as_json.pluck(:id)).to match_array(email_domain_blocks_ids[2..])
end
end
@@ -106,102 +91,78 @@ describe Api::V1::Admin::EmailDomainBlocksController do
let(:params) { { max_id: email_domain_blocks[3].id } }
it 'returns only the email domain blocks before max_id' do
- get :index, params: params
+ subject
email_domain_blocks_ids = email_domain_blocks.pluck(:id).map(&:to_s)
- json = body_as_json
- expect(json.pluck(:id)).to match_array(email_domain_blocks_ids[..2])
+ expect(body_as_json.pluck(:id)).to match_array(email_domain_blocks_ids[..2])
end
end
end
end
- describe 'GET #show' do
+ describe 'GET /api/v1/admin/email_domain_blocks/:id' do
+ subject do
+ get "/api/v1/admin/email_domain_blocks/#{email_domain_block.id}", headers: headers
+ end
+
let!(:email_domain_block) { Fabricate(:email_domain_block) }
- let(:params) { { id: email_domain_block.id } }
- context 'with wrong scope' do
- before do
- get :show, params: params
- end
-
- it_behaves_like 'forbidden for wrong scope', 'read:statuses'
- end
-
- context 'with wrong role' do
- before do
- get :show, params: params
- end
-
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
- end
+ it_behaves_like 'forbidden for wrong scope', 'read:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
context 'when email domain block exists' do
it 'returns http success' do
- get :show, params: params
+ subject
expect(response).to have_http_status(200)
end
it 'returns the correct blocked domain' do
- get :show, params: params
+ subject
- json = body_as_json
-
- expect(json[:domain]).to eq(email_domain_block.domain)
+ expect(body_as_json[:domain]).to eq(email_domain_block.domain)
end
end
context 'when email domain block does not exist' do
it 'returns http not found' do
- get :show, params: { id: 0 }
+ get '/api/v1/admin/email_domain_blocks/-1', headers: headers
expect(response).to have_http_status(404)
end
end
end
- describe 'POST #create' do
+ describe 'POST /api/v1/admin/email_domain_blocks' do
+ subject do
+ post '/api/v1/admin/email_domain_blocks', headers: headers, params: params
+ end
+
let(:params) { { domain: 'example.com' } }
- context 'with wrong scope' do
- before do
- post :create, params: params
- end
-
- it_behaves_like 'forbidden for wrong scope', 'read:statuses'
- end
-
- context 'with wrong role' do
- before do
- post :create, params: params
- end
-
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
- end
+ it_behaves_like 'forbidden for wrong scope', 'read:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
- post :create, params: params
+ subject
expect(response).to have_http_status(200)
end
it 'returns the correct blocked email domain' do
- post :create, params: params
+ subject
- json = body_as_json
-
- expect(json[:domain]).to eq(params[:domain])
+ expect(body_as_json[:domain]).to eq(params[:domain])
end
context 'when domain param is not provided' do
let(:params) { { domain: '' } }
it 'returns http unprocessable entity' do
- post :create, params: params
+ subject
expect(response).to have_http_status(422)
end
@@ -211,7 +172,7 @@ describe Api::V1::Admin::EmailDomainBlocksController do
let(:params) { { domain: 'do\uD800.com' } }
it 'returns http unprocessable entity' do
- post :create, params: params
+ subject
expect(response).to have_http_status(422)
end
@@ -223,59 +184,45 @@ describe Api::V1::Admin::EmailDomainBlocksController do
end
it 'returns http unprocessable entity' do
- post :create, params: params
+ subject
expect(response).to have_http_status(422)
end
end
end
- describe 'DELETE #destroy' do
+ describe 'DELETE /api/v1/admin/email_domain_blocks' do
+ subject do
+ delete "/api/v1/admin/email_domain_blocks/#{email_domain_block.id}", headers: headers
+ end
+
let!(:email_domain_block) { Fabricate(:email_domain_block) }
- let(:params) { { id: email_domain_block.id } }
- context 'with wrong scope' do
- before do
- delete :destroy, params: params
- end
-
- it_behaves_like 'forbidden for wrong scope', 'read:statuses'
- end
-
- context 'with wrong role' do
- before do
- delete :destroy, params: params
- end
-
- it_behaves_like 'forbidden for wrong role', ''
- it_behaves_like 'forbidden for wrong role', 'Moderator'
- end
+ it_behaves_like 'forbidden for wrong scope', 'read:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
- delete :destroy, params: params
+ subject
expect(response).to have_http_status(200)
end
it 'returns an empty body' do
- delete :destroy, params: params
+ subject
- json = body_as_json
-
- expect(json).to be_empty
+ expect(body_as_json).to be_empty
end
it 'deletes email domain block' do
- delete :destroy, params: params
+ subject
- email_domain_block = EmailDomainBlock.find_by(id: params[:id])
-
- expect(email_domain_block).to be_nil
+ expect(EmailDomainBlock.find_by(id: email_domain_block.id)).to be_nil
end
context 'when email domain block does not exist' do
it 'returns http not found' do
- delete :destroy, params: { id: 0 }
+ delete '/api/v1/admin/email_domain_blocks/-1', headers: headers
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/v1/admin/ip_blocks_spec.rb b/spec/requests/api/v1/admin/ip_blocks_spec.rb
new file mode 100644
index 0000000000..2091ef3dc6
--- /dev/null
+++ b/spec/requests/api/v1/admin/ip_blocks_spec.rb
@@ -0,0 +1,275 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'IP Blocks' do
+ let(:role) { UserRole.find_by(name: 'Admin') }
+ let(:user) { Fabricate(:user, role: role) }
+ let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+ let(:scopes) { 'admin:read:ip_blocks admin:write:ip_blocks' }
+ let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
+
+ shared_examples 'forbidden for wrong scope' do |wrong_scope|
+ let(:scopes) { wrong_scope }
+
+ it 'returns http forbidden' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ shared_examples 'forbidden for wrong role' do |wrong_role|
+ let(:role) { UserRole.find_by(name: wrong_role) }
+
+ it 'returns http forbidden' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'GET /api/v1/admin/ip_blocks' do
+ subject do
+ get '/api/v1/admin/ip_blocks', headers: headers, params: params
+ end
+
+ let(:params) { {} }
+
+ it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when there is no ip block' do
+ it 'returns an empty body' do
+ subject
+
+ expect(body_as_json).to be_empty
+ end
+ end
+
+ context 'when there are ip blocks' do
+ let!(:ip_blocks) do
+ [
+ IpBlock.create(ip: '192.0.2.0/24', severity: :no_access),
+ IpBlock.create(ip: '172.16.0.1', severity: :sign_up_requires_approval, comment: 'Spam'),
+ IpBlock.create(ip: '2001:0db8::/32', severity: :sign_up_block, expires_in: 10.days),
+ ]
+ end
+ let(:expected_response) do
+ ip_blocks.map do |ip_block|
+ {
+ id: ip_block.id.to_s,
+ ip: ip_block.ip,
+ severity: ip_block.severity.to_s,
+ comment: ip_block.comment,
+ created_at: ip_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
+ expires_at: ip_block.expires_at&.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
+ }
+ end
+ end
+
+ it 'returns the correct blocked ips' do
+ subject
+
+ expect(body_as_json).to match_array(expected_response)
+ end
+
+ context 'with limit param' do
+ let(:params) { { limit: 2 } }
+
+ it 'returns only the requested number of ip blocks' do
+ subject
+
+ expect(body_as_json.size).to eq(params[:limit])
+ end
+ end
+ end
+ end
+
+ describe 'GET /api/v1/admin/ip_blocks/:id' do
+ subject do
+ get "/api/v1/admin/ip_blocks/#{ip_block.id}", headers: headers
+ end
+
+ let!(:ip_block) { IpBlock.create(ip: '192.0.2.0/24', severity: :no_access) }
+
+ it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the correct ip block' do
+ subject
+
+ json = body_as_json
+
+ expect(json[:ip]).to eq("#{ip_block.ip}/#{ip_block.ip.prefix}")
+ expect(json[:severity]).to eq(ip_block.severity.to_s)
+ end
+
+ context 'when ip block does not exist' do
+ it 'returns http not found' do
+ get '/api/v1/admin/ip_blocks/-1', headers: headers
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /api/v1/admin/ip_blocks' do
+ subject do
+ post '/api/v1/admin/ip_blocks', headers: headers, params: params
+ end
+
+ let(:params) { { ip: '151.0.32.55', severity: 'no_access', comment: 'Spam' } }
+
+ it_behaves_like 'forbidden for wrong scope', 'admin:read:ip_blocks'
+ it_behaves_like 'forbidden for wrong role', ''
+ it_behaves_like 'forbidden for wrong role', 'Moderator'
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the correct ip block' do
+ subject
+
+ json = body_as_json
+
+ expect(json[:ip]).to eq("#{params[:ip]}/32")
+ expect(json[:severity]).to eq(params[:severity])
+ expect(json[:comment]).to eq(params[:comment])
+ end
+
+ context 'when the required ip param is not provided' do
+ let(:params) { { ip: '', severity: 'no_access' } }
+
+ it 'returns http unprocessable entity' do
+ subject
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'when the required severity param is not provided' do
+ let(:params) { { ip: '173.65.23.1', severity: '' } }
+
+ it 'returns http unprocessable entity' do
+ subject
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'when the given ip address is already blocked' do
+ before do
+ IpBlock.create(params)
+ end
+
+ it 'returns http unprocessable entity' do
+ subject
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'when the given ip address is invalid' do
+ let(:params) { { ip: '520.13.54.120', severity: 'no_access' } }
+
+ it 'returns http unprocessable entity' do
+ subject
+
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ describe 'PUT /api/v1/admin/ip_blocks/:id' do
+ subject do
+ put "/api/v1/admin/ip_blocks/#{ip_block.id}", headers: headers, params: params
+ end
+
+ let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access', comment: 'Spam', expires_in: 48.hours) }
+ let(:params) { { severity: 'sign_up_requires_approval', comment: 'Decreasing severity' } }
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the correct ip block' do
+ subject
+
+ expect(body_as_json).to match(hash_including({
+ ip: "#{ip_block.ip}/#{ip_block.ip.prefix}",
+ severity: 'sign_up_requires_approval',
+ comment: 'Decreasing severity',
+ }))
+ end
+
+ it 'updates the severity correctly' do
+ expect { subject }.to change { ip_block.reload.severity }.from('no_access').to('sign_up_requires_approval')
+ end
+
+ it 'updates the comment correctly' do
+ expect { subject }.to change { ip_block.reload.comment }.from('Spam').to('Decreasing severity')
+ end
+
+ context 'when ip block does not exist' do
+ it 'returns http not found' do
+ put '/api/v1/admin/ip_blocks/-1', headers: headers, params: params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'DELETE /api/v1/admin/ip_blocks/:id' do
+ subject do
+ delete "/api/v1/admin/ip_blocks/#{ip_block.id}", headers: headers
+ end
+
+ let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access') }
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns an empty body' do
+ subject
+
+ expect(body_as_json).to be_empty
+ end
+
+ it 'deletes the ip block' do
+ subject
+
+ expect(IpBlock.find_by(id: ip_block.id)).to be_nil
+ end
+
+ context 'when ip block does not exist' do
+ it 'returns http not found' do
+ delete '/api/v1/admin/ip_blocks/-1', headers: headers
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v1/admin/reports_spec.rb b/spec/requests/api/v1/admin/reports_spec.rb
new file mode 100644
index 0000000000..cd9fc100e7
--- /dev/null
+++ b/spec/requests/api/v1/admin/reports_spec.rb
@@ -0,0 +1,292 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Reports' do
+ let(:role) { UserRole.find_by(name: 'Admin') }
+ let(:user) { Fabricate(:user, role: role) }
+ let(:scopes) { 'admin:read:reports admin:write:reports' }
+ let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+ let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
+
+ shared_examples 'forbidden for wrong scope' do |wrong_scope|
+ let(:scopes) { wrong_scope }
+
+ it 'returns http forbidden' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ shared_examples 'forbidden for wrong role' do |wrong_role|
+ let(:role) { UserRole.find_by(name: wrong_role) }
+
+ it 'returns http forbidden' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'GET /api/v1/admin/reports' do
+ subject do
+ get '/api/v1/admin/reports', headers: headers, params: params
+ end
+
+ let(:params) { {} }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when there are no reports' do
+ it 'returns an empty list' do
+ subject
+
+ expect(body_as_json).to be_empty
+ end
+ end
+
+ context 'when there are reports' do
+ let!(:reporter) { Fabricate(:account) }
+ let!(:spammer) { Fabricate(:account) }
+ let(:expected_response) do
+ scope.map do |report|
+ hash_including({
+ id: report.id.to_s,
+ action_taken: report.action_taken?,
+ category: report.category,
+ comment: report.comment,
+ account: hash_including(id: report.account.id.to_s),
+ target_account: hash_including(id: report.target_account.id.to_s),
+ statuses: report.statuses,
+ rules: report.rules,
+ forwarded: report.forwarded,
+ })
+ end
+ end
+ let(:scope) { Report.unresolved }
+
+ before do
+ Fabricate(:report)
+ Fabricate(:report, target_account: spammer)
+ Fabricate(:report, account: reporter, target_account: spammer)
+ Fabricate(:report, action_taken_at: 4.days.ago, account: reporter)
+ Fabricate(:report, action_taken_at: 20.days.ago)
+ end
+
+ it 'returns all unresolved reports' do
+ subject
+
+ expect(body_as_json).to match_array(expected_response)
+ end
+
+ context 'with resolved param' do
+ let(:params) { { resolved: true } }
+ let(:scope) { Report.resolved }
+
+ it 'returns only the resolved reports' do
+ subject
+
+ expect(body_as_json).to match_array(expected_response)
+ end
+ end
+
+ context 'with account_id param' do
+ let(:params) { { account_id: reporter.id } }
+ let(:scope) { Report.unresolved.where(account: reporter) }
+
+ it 'returns all unresolved reports filed by the specified account' do
+ subject
+
+ expect(body_as_json).to match_array(expected_response)
+ end
+ end
+
+ context 'with target_account_id param' do
+ let(:params) { { target_account_id: spammer.id } }
+ let(:scope) { Report.unresolved.where(target_account: spammer) }
+
+ it 'returns all unresolved reports targeting the specified account' do
+ subject
+
+ expect(body_as_json).to match_array(expected_response)
+ end
+ end
+
+ context 'with limit param' do
+ let(:params) { { limit: 1 } }
+
+ it 'returns only the requested number of reports' do
+ subject
+
+ expect(body_as_json.size).to eq(1)
+ end
+ end
+ end
+ end
+
+ describe 'GET /api/v1/admin/reports/:id' do
+ subject do
+ get "/api/v1/admin/reports/#{report.id}", headers: headers
+ end
+
+ let(:report) { Fabricate(:report) }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the requested report content' do
+ subject
+
+ expect(body_as_json).to include(
+ {
+ id: report.id.to_s,
+ action_taken: report.action_taken?,
+ category: report.category,
+ comment: report.comment,
+ account: a_hash_including(id: report.account.id.to_s),
+ target_account: a_hash_including(id: report.target_account.id.to_s),
+ statuses: report.statuses,
+ rules: report.rules,
+ forwarded: report.forwarded,
+ }
+ )
+ end
+ end
+
+ describe 'PUT /api/v1/admin/reports/:id' do
+ subject do
+ put "/api/v1/admin/reports/#{report.id}", headers: headers, params: params
+ end
+
+ let!(:report) { Fabricate(:report, category: :other) }
+ let(:params) { { category: 'spam' } }
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'updates the report category' do
+ expect { subject }.to change { report.reload.category }.from('other').to('spam')
+ end
+
+ it 'returns the updated report content' do
+ subject
+
+ report.reload
+
+ expect(body_as_json).to include(
+ {
+ id: report.id.to_s,
+ action_taken: report.action_taken?,
+ category: report.category,
+ comment: report.comment,
+ account: a_hash_including(id: report.account.id.to_s),
+ target_account: a_hash_including(id: report.target_account.id.to_s),
+ statuses: report.statuses,
+ rules: report.rules,
+ forwarded: report.forwarded,
+ }
+ )
+ end
+ end
+
+ describe 'POST #resolve' do
+ subject do
+ post "/api/v1/admin/reports/#{report.id}/resolve", headers: headers
+ end
+
+ let(:report) { Fabricate(:report, action_taken_at: nil) }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'marks report as resolved' do
+ expect { subject }.to change { report.reload.unresolved? }.from(true).to(false)
+ end
+ end
+
+ describe 'POST #reopen' do
+ subject do
+ post "/api/v1/admin/reports/#{report.id}/reopen", headers: headers
+ end
+
+ let(:report) { Fabricate(:report, action_taken_at: 10.days.ago) }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'marks report as unresolved' do
+ expect { subject }.to change { report.reload.unresolved? }.from(false).to(true)
+ end
+ end
+
+ describe 'POST #assign_to_self' do
+ subject do
+ post "/api/v1/admin/reports/#{report.id}/assign_to_self", headers: headers
+ end
+
+ let(:report) { Fabricate(:report) }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'assigns report to the requesting user' do
+ expect { subject }.to change { report.reload.assigned_account_id }.from(nil).to(user.account.id)
+ end
+ end
+
+ describe 'POST #unassign' do
+ subject do
+ post "/api/v1/admin/reports/#{report.id}/unassign", headers: headers
+ end
+
+ let(:report) { Fabricate(:report, assigned_account_id: user.account.id) }
+
+ it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+ it_behaves_like 'forbidden for wrong role', ''
+
+ it 'returns http success' do
+ subject
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'unassigns report from assignee' do
+ expect { subject }.to change { report.reload.assigned_account_id }.from(user.account.id).to(nil)
+ end
+ end
+end
diff --git a/spec/services/translate_status_service_spec.rb b/spec/services/translate_status_service_spec.rb
index 074f55544a..515dd1a997 100644
--- a/spec/services/translate_status_service_spec.rb
+++ b/spec/services/translate_status_service_spec.rb
@@ -152,22 +152,31 @@ RSpec.describe TranslateStatusService, type: :service do
describe 'status has poll' do
let(:poll) { Fabricate(:poll, options: %w(Blue Green)) }
- it 'returns formatted poll options' do
- source_texts = service.send(:source_texts)
- expect(source_texts.size).to eq 3
- expect(source_texts.values).to eq %w(Hello
Blue Green)
+ context 'with source texts from the service' do
+ let!(:source_texts) { service.send(:source_texts) }
- expect(source_texts.keys.first).to eq :content
+ it 'returns formatted poll options' do
+ expect(source_texts.size).to eq 3
+ expect(source_texts.values).to eq %w(Hello
Blue Green)
+ end
- option1 = source_texts.keys.second
- expect(option1).to be_a Poll::Option
- expect(option1.id).to eq '0'
- expect(option1.title).to eq 'Blue'
+ it 'has a first key with content' do
+ expect(source_texts.keys.first).to eq :content
+ end
- option2 = source_texts.keys.third
- expect(option2).to be_a Poll::Option
- expect(option2.id).to eq '1'
- expect(option2.title).to eq 'Green'
+ it 'has the first option in the second key with correct options' do
+ option1 = source_texts.keys.second
+ expect(option1).to be_a Poll::Option
+ expect(option1.id).to eq '0'
+ expect(option1.title).to eq 'Blue'
+ end
+
+ it 'has the second option in the third key with correct options' do
+ option2 = source_texts.keys.third
+ expect(option2).to be_a Poll::Option
+ expect(option2.id).to eq '1'
+ expect(option2.title).to eq 'Green'
+ end
end
end
diff --git a/spec/support/examples/models/concerns/account_avatar.rb b/spec/support/examples/models/concerns/account_avatar.rb
index 2180f52739..16ebda5641 100644
--- a/spec/support/examples/models/concerns/account_avatar.rb
+++ b/spec/support/examples/models/concerns/account_avatar.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
shared_examples 'AccountAvatar' do |fabricator|
- describe 'static avatars' do
+ describe 'static avatars', paperclip_processing: true do
describe 'when GIF' do
it 'creates a png static style' do
account = Fabricate(fabricator, avatar: attachment_fixture('avatar.gif'))
@@ -17,7 +17,7 @@ shared_examples 'AccountAvatar' do |fabricator|
end
end
- describe 'base64-encoded files' do
+ describe 'base64-encoded files', paperclip_processing: true do
let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" }
let(:account) { Fabricate(fabricator, avatar: base64_attachment) }
diff --git a/spec/support/examples/models/concerns/account_header.rb b/spec/support/examples/models/concerns/account_header.rb
index 77ee0e6290..d65f54f00f 100644
--- a/spec/support/examples/models/concerns/account_header.rb
+++ b/spec/support/examples/models/concerns/account_header.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
shared_examples 'AccountHeader' do |fabricator|
- describe 'base64-encoded files' do
+ describe 'base64-encoded files', paperclip_processing: true do
let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" }
let(:account) { Fabricate(fabricator, header: base64_attachment) }
diff --git a/streaming/index.js b/streaming/index.js
index 279ebbad83..aabd2f8559 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -52,18 +52,31 @@ const redisUrlToClient = async (defaultConfig, redisUrl) => {
};
/**
+ * Attempts to safely parse a string as JSON, used when both receiving a message
+ * from redis and when receiving a message from a client over a websocket
+ * connection, this is why it accepts a `req` argument.
* @param {string} json
- * @param {any} req
+ * @param {any?} req
* @returns {Object.|null}
*/
const parseJSON = (json, req) => {
try {
return JSON.parse(json);
} catch (err) {
- if (req.accountId) {
- log.warn(req.requestId, `Error parsing message from user ${req.accountId}: ${err}`);
+ /* FIXME: This logging isn't great, and should probably be done at the
+ * call-site of parseJSON, not in the method, but this would require changing
+ * the signature of parseJSON to return something akin to a Result type:
+ * [Error|null, null|Object {
const { redisParams, redisUrl, redisPrefix } = redisConfigFromEnv(process.env);
/**
- * @type {Object.>}
+ * @type {Object.): void>>}
*/
const subs = {};
@@ -203,7 +216,10 @@ const startServer = async () => {
return;
}
- callbacks.forEach(callback => callback(message));
+ const json = parseJSON(message, null);
+ if (!json) return;
+
+ callbacks.forEach(callback => callback(json));
};
/**
@@ -225,7 +241,7 @@ const startServer = async () => {
/**
* @param {string} channel
- * @param {function(string): void} callback
+ * @param {function(Object): void} callback
*/
const unsubscribe = (channel, callback) => {
log.silly(`Removing listener for ${channel}`);
@@ -369,7 +385,7 @@ const startServer = async () => {
/**
* @param {any} req
- * @returns {string}
+ * @returns {string|undefined}
*/
const channelNameFromPath = req => {
const { path, query } = req;
@@ -478,15 +494,11 @@ const startServer = async () => {
/**
* @param {any} req
* @param {SystemMessageHandlers} eventHandlers
- * @returns {function(string): void}
+ * @returns {function(object): void}
*/
const createSystemMessageListener = (req, eventHandlers) => {
return message => {
- const json = parseJSON(message, req);
-
- if (!json) return;
-
- const { event } = json;
+ const { event } = message;
log.silly(req.requestId, `System message for ${req.accountId}: ${event}`);
@@ -603,19 +615,16 @@ const startServer = async () => {
* @param {function(string, string): void} output
* @param {function(string[], function(string): void): void} attachCloseHandler
* @param {boolean=} needsFiltering
- * @returns {function(string): void}
+ * @returns {function(object): void}
*/
const streamFrom = (ids, req, output, attachCloseHandler, needsFiltering = false) => {
const accountId = req.accountId || req.remoteAddress;
log.verbose(req.requestId, `Starting stream from ${ids.join(', ')} for ${accountId}`);
+ // Currently message is of type string, soon it'll be Record
const listener = message => {
- const json = parseJSON(message, req);
-
- if (!json) return;
-
- const { event, payload, queued_at } = json;
+ const { event, payload, queued_at } = message;
const transmit = () => {
const now = new Date().getTime();
@@ -819,7 +828,11 @@ const startServer = async () => {
return;
}
- ws.send(JSON.stringify({ stream: streamName, event, payload }));
+ ws.send(JSON.stringify({ stream: streamName, event, payload }), (err) => {
+ if (err) {
+ log.error(req.requestId, `Failed to send to websocket: ${err}`);
+ }
+ });
};
/**
@@ -1198,8 +1211,15 @@ const startServer = async () => {
ws.on('close', onEnd);
ws.on('error', onEnd);
- ws.on('message', data => {
- const json = parseJSON(data, session.request);
+ ws.on('message', (data, isBinary) => {
+ if (isBinary) {
+ log.warn('socket', 'Received binary data, closing connection');
+ ws.close(1003, 'The mastodon streaming server does not support binary messages');
+ return;
+ }
+ const message = data.toString('utf8');
+
+ const json = parseJSON(message, session.request);
if (!json) return;
diff --git a/yarn.lock b/yarn.lock
index d191e1489b..674ca94743 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1860,10 +1860,10 @@
picocolors "^1.0.0"
tslib "^2.5.0"
-"@polka/url@^1.0.0-next.9":
- version "1.0.0-next.11"
- resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71"
- integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA==
+"@polka/url@^1.0.0-next.20":
+ version "1.0.0-next.21"
+ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
+ integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
"@popperjs/core@^2.11.6":
version "2.11.6"
@@ -2275,11 +2275,16 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
-"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6":
+"@types/json-schema@^7.0.5":
version "7.0.6"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
+"@types/json-schema@^7.0.6", "@types/json-schema@^7.0.8":
+ version "7.0.12"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
+ integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
+
"@types/json-stable-stringify@^1.0.32":
version "1.0.34"
resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.34.tgz#c0fb25e4d957e0ee2e497c1f553d7f8bb668fd75"
@@ -4991,9 +4996,9 @@ domutils@^3.0.1:
domhandler "^5.0.1"
dotenv@^16.0.3:
- version "16.0.3"
- resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
- integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
+ version "16.1.4"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.1.4.tgz#67ac1a10cd9c25f5ba604e4e08bc77c0ebe0ca8c"
+ integrity sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw==
duplexer@^0.1.2:
version "0.1.2"
@@ -7786,9 +7791,9 @@ kleur@^3.0.3:
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
klona@^2.0.4:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
- integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22"
+ integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==
known-css-properties@^0.27.0:
version "0.27.0"
@@ -8234,7 +8239,7 @@ mime@1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
-mime@^2.3.1, mime@^2.4.4:
+mime@^2.4.4:
version "2.4.7"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.7.tgz#962aed9be0ed19c91fd7dc2ece5d7f4e89a90d74"
integrity sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==
@@ -8384,6 +8389,11 @@ mousetrap@^1.5.2:
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9"
integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==
+mrmime@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27"
+ integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -10429,9 +10439,9 @@ safe-regex@^1.1.0:
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass-loader@^10.2.0:
- version "10.2.0"
- resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.2.0.tgz#3d64c1590f911013b3fa48a0b22a83d5e1494716"
- integrity sha512-kUceLzC1gIHz0zNJPpqRsJyisWatGYNFRmv2CKZK2/ngMJgLqxTbXwe/hJ85luyvZkgqU3VlJ33UVF2T/0g6mw==
+ version "10.4.1"
+ resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.4.1.tgz#bea4e173ddf512c9d7f53e9ec686186146807cbf"
+ integrity sha512-aX/iJZTTpNUNx/OSYzo2KsjIUQHqvWsAhhUijFjAPdZTEhstjZI9zTNvkTTwsx+uNUJqUwOw5gacxQMx4hJxGQ==
dependencies:
klona "^2.0.4"
loader-utils "^2.0.0"
@@ -10480,7 +10490,7 @@ schema-utils@^2.6.5:
ajv "^6.12.4"
ajv-keywords "^3.5.2"
-schema-utils@^3.0, schema-utils@^3.0.0:
+schema-utils@^3.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef"
integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==
@@ -10489,6 +10499,15 @@ schema-utils@^3.0, schema-utils@^3.0.0:
ajv "^6.12.5"
ajv-keywords "^3.5.2"
+schema-utils@^3.0.0:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.2.tgz#36c10abca6f7577aeae136c804b0c741edeadc99"
+ integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==
+ dependencies:
+ "@types/json-schema" "^7.0.8"
+ ajv "^6.12.5"
+ ajv-keywords "^3.5.2"
+
scroll-behavior@^0.9.1:
version "0.9.12"
resolved "https://registry.yarnpkg.com/scroll-behavior/-/scroll-behavior-0.9.12.tgz#1c22d273ec4ce6cd4714a443fead50227da9424c"
@@ -10676,12 +10695,12 @@ signal-exit@^4.0.1:
integrity sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw==
sirv@^1.0.7:
- version "1.0.10"
- resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.10.tgz#3e591f5a9ae2520f50d5830f5fae38d97e7be194"
- integrity sha512-H5EZCoZaggEUQy8ocKsF7WAToGuZhjJlLvM3XOef46CbdIgbNeQ1p32N1PCuCjkVYwrAVOSMacN6CXXgIzuspg==
+ version "1.0.19"
+ resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49"
+ integrity sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==
dependencies:
- "@polka/url" "^1.0.0-next.9"
- mime "^2.3.1"
+ "@polka/url" "^1.0.0-next.20"
+ mrmime "^1.0.0"
totalist "^1.0.0"
sisteransi@^1.0.4:
@@ -11844,9 +11863,9 @@ update-browserslist-db@^1.0.10:
picocolors "^1.0.0"
uri-js@^4.2.2:
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602"
- integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+ integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
dependencies:
punycode "^2.1.0"
@@ -12065,9 +12084,9 @@ webpack-assets-manifest@^4.0.6:
webpack-sources "^1.0"
webpack-bundle-analyzer@^4.8.0:
- version "4.8.0"
- resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.8.0.tgz#951b8aaf491f665d2ae325d8b84da229157b1d04"
- integrity sha512-ZzoSBePshOKhr+hd8u6oCkZVwpVaXgpw23ScGLFpR6SjYI7+7iIWYarjN6OEYOfRt8o7ZyZZQk0DuMizJ+LEIg==
+ version "4.9.0"
+ resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.0.tgz#fc093c4ab174fd3dcbd1c30b763f56d10141209d"
+ integrity sha512-+bXGmO1LyiNx0i9enBu3H8mv42sj/BJWhZNFwjz92tVnBa9J3JMGo2an2IXlEleoDOPn/Hofl5hr/xCpObUDtw==
dependencies:
"@discoveryjs/json-ext" "0.5.7"
acorn "^8.0.4"
@@ -12558,9 +12577,9 @@ ws@^6.2.1:
async-limiter "~1.0.0"
ws@^7.3.1:
- version "7.4.6"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
- integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
+ version "7.5.9"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
+ integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
ws@^8.11.0, ws@^8.12.1, ws@^8.13.0:
version "8.13.0"