Merge commit '77cd16f4ee7ab807df6fffb1538a6659a8182a9e' into glitch-soc/merge-upstream

Conflicts:
- `app/javascript/styles/mastodon/components.scss`:
  Conflict caused by glitch-soc changing the path to images, and upstream
  removing styling using such an image.
  Removed the styling as upstream did.
- `app/models/account.rb`:
  Conflict due to upstream changing lines adjacent to a change made in glitch-soc
  to have configurable limits.
  Ported upstream's changes.
- `yarn.lock`:
  Dependencies adjacent to glitch-soc-only dependencies updated.
  Updated them as well.
This commit is contained in:
Claire 2024-10-26 13:38:07 +02:00
commit 8103e69b17
78 changed files with 2363 additions and 2051 deletions

View file

@ -18,7 +18,7 @@ permissions:
jobs: jobs:
check-i18n: check-i18n:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -191,7 +191,7 @@ FROM build AS libvips
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"] # libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips # renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
ARG VIPS_VERSION=8.15.3 ARG VIPS_VERSION=8.15.5
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"] # libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download ARG VIPS_URL=https://github.com/libvips/libvips/releases/download

View file

@ -61,7 +61,7 @@ gem 'irb', '~> 1.8'
gem 'kaminari', '~> 1.2' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar' gem 'mime-types', '~> 3.6.0', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.15' gem 'nokogiri', '~> 1.15'
gem 'oj', '~> 3.14' gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14' gem 'ox', '~> 2.14'
@ -111,8 +111,8 @@ group :opentelemetry do
gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false gem 'opentelemetry-instrumentation-rack', '~> 0.25.0', require: false
gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false gem 'opentelemetry-instrumentation-rails', '~> 0.32.0', require: false
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
gem 'opentelemetry-sdk', '~> 1.4', require: false gem 'opentelemetry-sdk', '~> 1.4', require: false

View file

@ -100,17 +100,17 @@ GEM
attr_required (1.0.2) attr_required (1.0.2)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.983.0) aws-partitions (1.992.0)
aws-sdk-core (3.209.1) aws-sdk-core (3.210.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.94.0) aws-sdk-kms (1.95.0)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.167.0) aws-sdk-s3 (1.169.0)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.0) aws-sigv4 (1.10.0)
@ -137,7 +137,7 @@ GEM
blurhash (0.1.8) blurhash (0.1.8)
bootsnap (1.18.4) bootsnap (1.18.4)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (6.2.1) brakeman (6.2.2)
racc racc
browser (5.3.1) browser (5.3.1)
brpoplpush-redis_script (0.1.3) brpoplpush-redis_script (0.1.3)
@ -233,7 +233,7 @@ GEM
tzinfo tzinfo
excon (0.111.0) excon (0.111.0)
fabrication (2.31.0) fabrication (2.31.0)
faker (3.4.2) faker (3.5.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.3) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
@ -429,9 +429,10 @@ GEM
azure-storage-blob (~> 2.0.1) azure-storage-blob (~> 2.0.1)
hashie (~> 5.0) hashie (~> 5.0)
memory_profiler (1.1.0) memory_profiler (1.1.0)
mime-types (3.5.2) mime-types (3.6.0)
logger
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2024.0820) mime-types-data (3.2024.1001)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.7) mini_portile2 (2.8.7)
minitest (5.25.1) minitest (5.25.1)
@ -503,7 +504,7 @@ GEM
opentelemetry-semantic_conventions opentelemetry-semantic_conventions
opentelemetry-helpers-sql-obfuscation (0.2.0) opentelemetry-helpers-sql-obfuscation (0.2.0)
opentelemetry-common (~> 0.21) opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.1.0) opentelemetry-instrumentation-action_mailer (0.2.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.1) opentelemetry-instrumentation-active_support (~> 0.1)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
@ -515,13 +516,13 @@ GEM
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.1) opentelemetry-instrumentation-active_support (~> 0.1)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_job (0.7.7) opentelemetry-instrumentation-active_job (0.7.8)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_model_serializers (0.20.2) opentelemetry-instrumentation-active_model_serializers (0.20.2)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_record (0.7.3) opentelemetry-instrumentation-active_record (0.8.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_support (0.6.0) opentelemetry-instrumentation-active_support (0.6.0)
@ -553,16 +554,16 @@ GEM
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql-obfuscation opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rack (0.24.6) opentelemetry-instrumentation-rack (0.25.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rails (0.31.2) opentelemetry-instrumentation-rails (0.32.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-action_mailer (~> 0.1.0) opentelemetry-instrumentation-action_mailer (~> 0.2.0)
opentelemetry-instrumentation-action_pack (~> 0.9.0) opentelemetry-instrumentation-action_pack (~> 0.9.0)
opentelemetry-instrumentation-action_view (~> 0.7.0) opentelemetry-instrumentation-action_view (~> 0.7.0)
opentelemetry-instrumentation-active_job (~> 0.7.0) opentelemetry-instrumentation-active_job (~> 0.7.0)
opentelemetry-instrumentation-active_record (~> 0.7.0) opentelemetry-instrumentation-active_record (~> 0.8.0)
opentelemetry-instrumentation-active_support (~> 0.6.0) opentelemetry-instrumentation-active_support (~> 0.6.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-redis (0.25.7) opentelemetry-instrumentation-redis (0.25.7)
@ -590,7 +591,7 @@ GEM
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.5.8) pg (1.5.9)
pghero (3.6.1) pghero (3.6.1)
activerecord (>= 6.1) activerecord (>= 6.1)
premailer (1.27.0) premailer (1.27.0)
@ -761,7 +762,7 @@ GEM
rubocop-rspec_rails (2.30.0) rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61) rubocop (~> 1.61)
rubocop-rspec (~> 3, >= 3.0.1) rubocop-rspec (~> 3, >= 3.0.1)
ruby-prof (1.7.0) ruby-prof (1.7.1)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-saml (1.17.0) ruby-saml (1.17.0)
nokogiri (>= 1.13.10) nokogiri (>= 1.13.10)
@ -970,7 +971,7 @@ DEPENDENCIES
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
md-paperclip-azure (~> 2.2) md-paperclip-azure (~> 2.2)
memory_profiler memory_profiler
mime-types (~> 3.5.0) mime-types (~> 3.6.0)
net-http (~> 0.4.0) net-http (~> 0.4.0)
net-ldap (~> 0.18) net-ldap (~> 0.18)
nokogiri (~> 1.15) nokogiri (~> 1.15)
@ -991,8 +992,8 @@ DEPENDENCIES
opentelemetry-instrumentation-http_client (~> 0.22.3) opentelemetry-instrumentation-http_client (~> 0.22.3)
opentelemetry-instrumentation-net_http (~> 0.22.4) opentelemetry-instrumentation-net_http (~> 0.22.4)
opentelemetry-instrumentation-pg (~> 0.29.0) opentelemetry-instrumentation-pg (~> 0.29.0)
opentelemetry-instrumentation-rack (~> 0.24.1) opentelemetry-instrumentation-rack (~> 0.25.0)
opentelemetry-instrumentation-rails (~> 0.31.0) opentelemetry-instrumentation-rails (~> 0.32.0)
opentelemetry-instrumentation-redis (~> 0.25.3) opentelemetry-instrumentation-redis (~> 0.25.3)
opentelemetry-instrumentation-sidekiq (~> 0.25.2) opentelemetry-instrumentation-sidekiq (~> 0.25.2)
opentelemetry-sdk (~> 1.4) opentelemetry-sdk (~> 1.4)
@ -1057,7 +1058,7 @@ DEPENDENCIES
xorcist (~> 1.1) xorcist (~> 1.1)
RUBY VERSION RUBY VERSION
ruby 3.3.4p94 ruby 3.3.5p100
BUNDLED WITH BUNDLED WITH
2.5.18 2.5.22

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::Web::PushSubscriptionsController < Api::Web::BaseController class Api::Web::PushSubscriptionsController < Api::Web::BaseController
before_action :require_user! before_action :require_user!, except: :destroy
before_action :set_push_subscription, only: :update before_action :set_push_subscription, only: :update
before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions? before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions?
after_action :update_session_with_subscription, only: :create after_action :update_session_with_subscription, only: :create
@ -17,6 +17,13 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end end
def destroy
push_subscription = ::Web::PushSubscription.find_by_token_for(:unsubscribe, params[:id])
push_subscription&.destroy
head 200
end
private private
def active_session def active_session

View file

@ -10,7 +10,7 @@ module Auth::CaptchaConcern
end end
def captcha_available? def captcha_available?
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present? Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present?
end end
def captcha_enabled? def captcha_enabled?

View file

@ -2,7 +2,7 @@
module Admin::SettingsHelper module Admin::SettingsHelper
def captcha_available? def captcha_available?
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present? Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present?
end end
def login_activity_title(activity) def login_activity_title(activity)

View file

@ -120,18 +120,6 @@ module ApplicationHelper
inline_svg_tag 'check.svg' inline_svg_tag 'check.svg'
end end
def visibility_icon(status)
if status.public_visibility?
material_symbol('globe', title: I18n.t('statuses.visibilities.public'))
elsif status.unlisted_visibility?
material_symbol('lock_open', title: I18n.t('statuses.visibilities.unlisted'))
elsif status.private_visibility? || status.limited_visibility?
material_symbol('lock', title: I18n.t('statuses.visibilities.private'))
elsif status.direct_visibility?
material_symbol('alternate_email', title: I18n.t('statuses.visibilities.direct'))
end
end
def interrelationships_icon(relationships, account_id) def interrelationships_icon(relationships, account_id)
if relationships.following[account_id] && relationships.followed_by[account_id] if relationships.following[account_id] && relationships.followed_by[account_id]
material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive') material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive')

View file

@ -162,7 +162,7 @@ module LanguagesHelper
th: ['Thai', 'ไทย'].freeze, th: ['Thai', 'ไทย'].freeze,
ti: ['Tigrinya', 'ትግርኛ'].freeze, ti: ['Tigrinya', 'ትግርኛ'].freeze,
tk: ['Turkmen', 'Türkmen'].freeze, tk: ['Turkmen', 'Türkmen'].freeze,
tl: ['Tagalog', 'Wikang Tagalog'].freeze, tl: ['Tagalog', 'Tagalog'].freeze,
tn: ['Tswana', 'Setswana'].freeze, tn: ['Tswana', 'Setswana'].freeze,
to: ['Tonga', 'faka Tonga'].freeze, to: ['Tonga', 'faka Tonga'].freeze,
tr: ['Turkish', 'Türkçe'].freeze, tr: ['Turkish', 'Türkçe'].freeze,

View file

@ -327,31 +327,24 @@ Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
if (!input) return; if (!input) return;
const oldReadOnly = input.readOnly; navigator.clipboard
.writeText(input.value)
input.readOnly = false; .then(() => {
input.focus();
input.select();
input.setSelectionRange(0, input.value.length);
try {
if (document.execCommand('copy')) {
input.blur();
const parent = target.parentElement; const parent = target.parentElement;
if (!parent) return; if (parent) {
parent.classList.add('copied'); parent.classList.add('copied');
setTimeout(() => { setTimeout(() => {
parent.classList.remove('copied'); parent.classList.remove('copied');
}, 700); }, 700);
} }
} catch (err) {
console.error(err);
}
input.readOnly = oldReadOnly; return true;
})
.catch((error: unknown) => {
console.error(error);
});
}); });
const toggleSidebar = () => { const toggleSidebar = () => {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg"><symbol id="mastodon-svg-logo" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" /></symbol></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="20" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.933 2.82414C22.324 4.07931 21.0726 5.3569 20.1788 6.6569C19.3296 7.91207 18.905 9.1 18.905 10.2207C19.0838 10.131 19.3073 10.0862 19.5754 10.0862C19.8883 10.0414 20.1564 10.019 20.3799 10.019C21.4078 10.019 22.257 10.4448 22.9274 11.2966C23.6425 12.1034 24 13.1121 24 14.3224C24 15.8017 23.5084 17.0345 22.5251 18.0207C21.5419 19.0069 20.3129 19.5 18.838 19.5C17.2737 19.5 16.0447 18.9397 15.1508 17.819C14.257 16.6535 13.8101 15.1069 13.8101 13.1793C13.8101 10.8931 14.5028 8.62931 15.8883 6.38793C17.2737 4.14655 19.3073 2.01724 21.9888 0L23.933 2.82414ZM10.1229 2.82414C8.51397 4.07931 7.26257 5.3569 6.36872 6.6569C5.51955 7.91207 5.09497 9.1 5.09497 10.2207C5.27374 10.131 5.49721 10.0862 5.76536 10.0862C6.07821 10.0414 6.34637 10.019 6.56983 10.019C7.59777 10.019 8.44693 10.4448 9.11732 11.2966C9.8324 12.1034 10.1899 13.1121 10.1899 14.3224C10.1899 15.8017 9.69832 17.0345 8.71508 18.0207C7.73184 19.0069 6.50279 19.5 5.02793 19.5C3.46369 19.5 2.23464 18.9397 1.34078 17.819C0.446927 16.6535 0 15.1069 0 13.1793C0 10.8931 0.692738 8.62931 2.07821 6.38793C3.46369 4.14655 5.49721 2.01724 8.17877 0L10.1229 2.82414Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,4 +1,4 @@
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren, JSX } from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';

View file

@ -8,7 +8,7 @@ export const ContentWarning: React.FC<{
<StatusBanner <StatusBanner
expanded={expanded} expanded={expanded}
onClick={onClick} onClick={onClick}
variant={BannerVariant.Yellow} variant={BannerVariant.Warning}
> >
<p dangerouslySetInnerHTML={{ __html: text }} /> <p dangerouslySetInnerHTML={{ __html: text }} />
</StatusBanner> </StatusBanner>

View file

@ -10,13 +10,16 @@ export const FilterWarning: React.FC<{
<StatusBanner <StatusBanner
expanded={expanded} expanded={expanded}
onClick={onClick} onClick={onClick}
variant={BannerVariant.Blue} variant={BannerVariant.Filter}
> >
<p> <p>
<FormattedMessage <FormattedMessage
id='filter_warning.matches_filter' id='filter_warning.matches_filter'
defaultMessage='Matches filter “{title}”' defaultMessage='Matches filter “<span>{title}</span>”'
values={{ title }} values={{
title,
span: (chunks) => <span className='filter-name'>{chunks}</span>,
}}
/> />
</p> </p>
</StatusBanner> </StatusBanner>

View file

@ -1,4 +1,5 @@
import { memo } from 'react'; import { memo } from 'react';
import type { JSX } from 'react';
import { FormattedMessage, FormattedNumber } from 'react-intl'; import { FormattedMessage, FormattedNumber } from 'react-intl';

View file

@ -449,7 +449,7 @@ class Status extends ImmutablePureComponent {
} else if (status.get('media_attachments').size > 0) { } else if (status.get('media_attachments').size > 0) {
const language = status.getIn(['translation', 'language']) || status.get('language'); const language = status.getIn(['translation', 'language']) || status.get('language');
if (['image', 'gifv'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) { if (['image', 'gifv', 'unknown'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
media = ( media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}> <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => ( {Component => (

View file

@ -1,8 +1,8 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
export enum BannerVariant { export enum BannerVariant {
Yellow = 'yellow', Warning = 'warning',
Blue = 'blue', Filter = 'filter',
} }
export const StatusBanner: React.FC<{ export const StatusBanner: React.FC<{
@ -11,9 +11,9 @@ export const StatusBanner: React.FC<{
expanded?: boolean; expanded?: boolean;
onClick?: () => void; onClick?: () => void;
}> = ({ children, variant, expanded, onClick }) => ( }> = ({ children, variant, expanded, onClick }) => (
<div <label
className={ className={
variant === BannerVariant.Yellow variant === BannerVariant.Warning
? 'content-warning' ? 'content-warning'
: 'content-warning content-warning--filter' : 'content-warning content-warning--filter'
} }
@ -26,6 +26,11 @@ export const StatusBanner: React.FC<{
id='content_warning.hide' id='content_warning.hide'
defaultMessage='Hide post' defaultMessage='Hide post'
/> />
) : variant === BannerVariant.Warning ? (
<FormattedMessage
id='content_warning.show_more'
defaultMessage='Show more'
/>
) : ( ) : (
<FormattedMessage <FormattedMessage
id='content_warning.show' id='content_warning.show'
@ -33,5 +38,5 @@ export const StatusBanner: React.FC<{
/> />
)} )}
</button> </button>
</div> </label>
); );

View file

@ -1,3 +1,5 @@
import type { JSX } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';

View file

@ -1,4 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { JSX } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';

View file

@ -197,6 +197,7 @@
"confirmations.unfollow.title": "Unfollow user?", "confirmations.unfollow.title": "Unfollow user?",
"content_warning.hide": "Hide post", "content_warning.hide": "Hide post",
"content_warning.show": "Show anyway", "content_warning.show": "Show anyway",
"content_warning.show_more": "Show more",
"conversation.delete": "Delete conversation", "conversation.delete": "Delete conversation",
"conversation.mark_as_read": "Mark as read", "conversation.mark_as_read": "Mark as read",
"conversation.open": "View conversation", "conversation.open": "View conversation",
@ -305,7 +306,7 @@
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one", "filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
"filter_modal.select_filter.title": "Filter this post", "filter_modal.select_filter.title": "Filter this post",
"filter_modal.title.status": "Filter a post", "filter_modal.title.status": "Filter a post",
"filter_warning.matches_filter": "Matches filter “{title}”", "filter_warning.matches_filter": "Matches filter “<span>{title}</span>”",
"filtered_notifications_banner.pending_requests": "From {count, plural, =0 {no one} one {one person} other {# people}} you may know", "filtered_notifications_banner.pending_requests": "From {count, plural, =0 {no one} one {one person} other {# people}} you may know",
"filtered_notifications_banner.title": "Filtered notifications", "filtered_notifications_banner.title": "Filtered notifications",
"firehose.all": "All", "firehose.all": "All",

View file

@ -57,7 +57,10 @@ export const accountsReducer: Reducer<typeof initialState> = (
return state.setIn([action.payload.id, 'hidden'], false); return state.setIn([action.payload.id, 'hidden'], false);
else if (importAccounts.match(action)) else if (importAccounts.match(action))
return normalizeAccounts(state, action.payload.accounts); return normalizeAccounts(state, action.payload.accounts);
else if (followAccountSuccess.match(action)) { else if (
followAccountSuccess.match(action) &&
!action.payload.alreadyFollowing
) {
return state return state
.update(action.payload.relationship.id, (account) => .update(action.payload.relationship.id, (account) =>
account?.update('followers_count', (n) => n + 1), account?.update('followers_count', (n) => n + 1),

View file

@ -76,4 +76,7 @@ body {
--background-color-tint: rgba(255, 255, 255, 80%); --background-color-tint: rgba(255, 255, 255, 80%);
--background-filter: blur(10px); --background-filter: blur(10px);
--on-surface-color: #{transparentize($ui-base-color, 0.65)}; --on-surface-color: #{transparentize($ui-base-color, 0.65)};
--rich-text-container-color: rgba(255, 216, 231, 100%);
--rich-text-text-color: rgba(114, 47, 83, 100%);
--rich-text-decorations-color: rgba(255, 175, 212, 100%);
} }

View file

@ -11109,19 +11109,21 @@ noscript {
} }
.content-warning { .content-warning {
display: block;
box-sizing: border-box; box-sizing: border-box;
background: rgba($ui-highlight-color, 0.05); background: rgba($ui-highlight-color, 0.05);
color: $secondary-text-color; color: $secondary-text-color;
border-top: 1px solid; border: 1px solid rgba($ui-highlight-color, 0.15);
border-bottom: 1px solid; border-radius: 8px;
border-color: rgba($ui-highlight-color, 0.15);
padding: 8px (5px + 8px); padding: 8px (5px + 8px);
position: relative; position: relative;
font-size: 15px; font-size: 15px;
line-height: 22px; line-height: 22px;
cursor: pointer;
p { p {
margin-bottom: 8px; margin-bottom: 8px;
font-weight: 500;
} }
.link-button { .link-button {
@ -11130,31 +11132,16 @@ noscript {
font-weight: 500; font-weight: 500;
} }
&::before, &--filter {
&::after { color: $darker-text-color;
content: '';
display: block; p {
position: absolute; font-weight: normal;
height: 100%;
background: url('~images/warning-stripes.svg') repeat-y;
width: 5px;
top: 0;
} }
&::before { .filter-name {
border-start-start-radius: 4px; font-weight: 500;
border-end-start-radius: 4px; color: $secondary-text-color;
inset-inline-start: 0; }
}
&::after {
border-start-end-radius: 4px;
border-end-end-radius: 4px;
inset-inline-end: 0;
}
&--filter::before,
&--filter::after {
background-image: url('~images/filter-stripes.svg');
} }
} }

View file

@ -2,9 +2,29 @@
.e-content, .e-content,
.edit-indicator__content, .edit-indicator__content,
.reply-indicator__content { .reply-indicator__content {
code {
background: var(--rich-text-container-color);
padding: 4px;
border-radius: 4px;
color: var(--rich-text-text-color);
font-size: 0.85em;
}
pre {
background: var(--rich-text-container-color);
padding: 8px;
border-radius: 4px;
color: var(--rich-text-text-color);
code {
padding: 0;
background: transparent;
}
}
pre, pre,
blockquote { blockquote {
margin-bottom: 20px; margin-bottom: 22px;
white-space: pre-wrap; white-space: pre-wrap;
unicode-bidi: plaintext; unicode-bidi: plaintext;
@ -14,19 +34,45 @@
} }
blockquote { blockquote {
padding-inline-start: 10px; padding-inline-start: 32px;
border-inline-start: 3px solid $darker-text-color; color: var(--rich-text-text-color);
color: $darker-text-color;
white-space: normal; white-space: normal;
position: relative;
p:last-child { &::before {
display: block;
content: '';
width: 24px;
height: 20px;
mask-image: url('../images/quote.svg');
background-color: var(--rich-text-decorations-color);
position: absolute;
inset-inline-start: 0;
top: 0;
}
blockquote {
margin-top: 4px;
border-inline-start: 3px solid var(--rich-text-decorations-color);
padding-inline-start: 16px;
&::before {
display: none;
}
}
p:last-of-type {
margin-bottom: 0; margin-bottom: 0;
} }
} }
& > ul, & > ul,
& > ol { & > ol {
margin-bottom: 20px; margin-bottom: 22px;
&:last-child {
margin-bottom: 0;
}
} }
b, b,
@ -41,7 +87,15 @@
ul, ul,
ol { ol {
margin-inline-start: 2em; padding-inline-start: 24px;
li {
padding-inline-start: 8px;
&::marker {
text-align: end;
}
}
p { p {
margin: 0; margin: 0;
@ -49,7 +103,11 @@
} }
ul { ul {
list-style-type: disc; list-style-type: '';
li::marker {
text-align: start;
}
} }
ol { ol {

View file

@ -116,4 +116,7 @@ $font-monospace: 'mastodon-font-monospace' !default;
--error-background-color: #{darken($error-red, 16%)}; --error-background-color: #{darken($error-red, 16%)};
--error-active-background-color: #{darken($error-red, 12%)}; --error-active-background-color: #{darken($error-red, 12%)};
--on-error-color: #fff; --on-error-color: #fff;
--rich-text-container-color: rgba(87, 24, 60, 100%);
--rich-text-text-color: rgba(255, 175, 212, 100%);
--rich-text-decorations-color: rgba(128, 58, 95, 100%);
} }

View file

@ -8,17 +8,27 @@ class TranslationService
class UnexpectedResponseError < Error; end class UnexpectedResponseError < Error; end
def self.configured def self.configured
if ENV['DEEPL_API_KEY'].present? if configuration.deepl[:api_key].present?
TranslationService::DeepL.new(ENV.fetch('DEEPL_PLAN', 'free'), ENV['DEEPL_API_KEY']) TranslationService::DeepL.new(
elsif ENV['LIBRE_TRANSLATE_ENDPOINT'].present? configuration.deepl[:plan],
TranslationService::LibreTranslate.new(ENV['LIBRE_TRANSLATE_ENDPOINT'], ENV['LIBRE_TRANSLATE_API_KEY']) configuration.deepl[:api_key]
)
elsif configuration.libre_translate[:endpoint].present?
TranslationService::LibreTranslate.new(
configuration.libre_translate[:endpoint],
configuration.libre_translate[:api_key]
)
else else
raise NotConfiguredError raise NotConfiguredError
end end
end end
def self.configured? def self.configured?
ENV['DEEPL_API_KEY'].present? || ENV['LIBRE_TRANSLATE_ENDPOINT'].present? configuration.deepl[:api_key].present? || configuration.libre_translate[:endpoint].present?
end
def self.configuration
Rails.configuration.x.translation
end end
def languages def languages

View file

@ -65,6 +65,8 @@ class Account < ApplicationRecord
) )
BACKGROUND_REFRESH_INTERVAL = 1.week.freeze BACKGROUND_REFRESH_INTERVAL = 1.week.freeze
REFRESH_DEADLINE = 6.hours
STALE_THRESHOLD = 1.day
DEFAULT_FIELDS_SIZE = (ENV['MAX_PROFILE_FIELDS'] || 4).to_i DEFAULT_FIELDS_SIZE = (ENV['MAX_PROFILE_FIELDS'] || 4).to_i
INSTANCE_ACTOR_ID = -99 INSTANCE_ACTOR_ID = -99
@ -229,13 +231,13 @@ class Account < ApplicationRecord
end end
def possibly_stale? def possibly_stale?
last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago last_webfingered_at.nil? || last_webfingered_at <= STALE_THRESHOLD.ago
end end
def schedule_refresh_if_stale! def schedule_refresh_if_stale!
return unless last_webfingered_at.present? && last_webfingered_at <= BACKGROUND_REFRESH_INTERVAL.ago return unless last_webfingered_at.present? && last_webfingered_at <= BACKGROUND_REFRESH_INTERVAL.ago
AccountRefreshWorker.perform_in(rand(6.hours.to_i), id) AccountRefreshWorker.perform_in(rand(REFRESH_DEADLINE), id)
end end
def refresh! def refresh!

View file

@ -36,9 +36,14 @@ class IpBlock < ApplicationRecord
class << self class << self
def blocked?(remote_ip) def blocked?(remote_ip)
blocked_ips_map = Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(IpBlock.where(severity: :no_access).pluck(:ip)) }
blocked_ips_map.include?(remote_ip) blocked_ips_map.include?(remote_ip)
end end
private
def blocked_ips_map
Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(severity_no_access.pluck(:ip)) }
end
end end
private private

View file

@ -19,6 +19,8 @@ class LinkFeed < PublicFeed
scope.merge!(discoverable) scope.merge!(discoverable)
scope.merge!(attached_to_preview_card) scope.merge!(attached_to_preview_card)
scope.merge!(account_filters_scope) if account?
scope.merge!(language_scope) if account&.chosen_languages.present?
scope.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id) scope.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
end end

View file

@ -29,6 +29,8 @@ class Web::PushSubscription < ApplicationRecord
delegate :locale, to: :associated_user delegate :locale, to: :associated_user
generates_token_for :unsubscribe, expires_in: Web::PushNotificationWorker::TTL
def pushable?(notification) def pushable?(notification)
policy_allows_notification?(notification) && alert_enabled_for_notification_type?(notification) policy_allows_notification?(notification) && alert_enabled_for_notification_type?(notification)
end end

View file

@ -2,10 +2,11 @@
class Web::PushNotificationWorker class Web::PushNotificationWorker
include Sidekiq::Worker include Sidekiq::Worker
include RoutingHelper
sidekiq_options queue: 'push', retry: 5 sidekiq_options queue: 'push', retry: 5
TTL = 48.hours.to_s TTL = 48.hours
URGENCY = 'normal' URGENCY = 'normal'
def perform(subscription_id, notification_id) def perform(subscription_id, notification_id)
@ -23,12 +24,13 @@ class Web::PushNotificationWorker
request.add_headers( request.add_headers(
'Content-Type' => 'application/octet-stream', 'Content-Type' => 'application/octet-stream',
'Ttl' => TTL, 'Ttl' => TTL.to_s,
'Urgency' => URGENCY, 'Urgency' => URGENCY,
'Content-Encoding' => 'aesgcm', 'Content-Encoding' => 'aesgcm',
'Encryption' => "salt=#{Webpush.encode64(payload.fetch(:salt)).delete('=')}", 'Encryption' => "salt=#{Webpush.encode64(payload.fetch(:salt)).delete('=')}",
'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{web_push_request.crypto_key_header}", 'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{web_push_request.crypto_key_header}",
'Authorization' => web_push_request.authorization_header 'Authorization' => web_push_request.authorization_header,
'Unsubscribe-URL' => subscription_url
) )
request.perform do |response| request.perform do |response|
@ -72,4 +74,8 @@ class Web::PushNotificationWorker
def request_pool def request_pool
RequestPool.current RequestPool.current
end end
def subscription_url
api_web_push_subscription_url(id: @subscription.generate_token_for(:unsubscribe))
end
end end

View file

@ -109,6 +109,9 @@ module Mastodon
end end
end end
config.x.captcha = config_for(:captcha)
config.x.translation = config_for(:translation)
config.to_prepare do config.to_prepare do
Doorkeeper::AuthorizationsController.layout 'modal' Doorkeeper::AuthorizationsController.layout 'modal'
Doorkeeper::AuthorizedApplicationsController.layout 'admin' Doorkeeper::AuthorizedApplicationsController.layout 'admin'

3
config/captcha.yml Normal file
View file

@ -0,0 +1,3 @@
shared:
secret_key: <%= ENV.fetch('HCAPTCHA_SECRET_KEY', nil) %>
site_key: <%= ENV.fetch('HCAPTCHA_SITE_KEY', nil) %>

View file

@ -16,7 +16,7 @@ Rails.application.configure do
# Show full error reports. # Show full error reports.
config.consider_all_requests_local = true config.consider_all_requests_local = true
# Enable server timing # Enable server timing.
config.server_timing = true config.server_timing = true
# Enable/disable caching. By default caching is disabled. # Enable/disable caching. By default caching is disabled.
@ -77,9 +77,6 @@ Rails.application.configure do
# Annotate rendered view with file names. # Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true # config.action_view.annotate_rendered_view_with_filenames = true
# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
config.action_mailer.default_options = { from: 'notifications@localhost' } config.action_mailer.default_options = { from: 'notifications@localhost' }
# If using a Heroku, Vagrant or generic remote development environment, # If using a Heroku, Vagrant or generic remote development environment,
@ -90,7 +87,7 @@ Rails.application.configure do
# TODO: Remove once devise-two-factor data migration complete # TODO: Remove once devise-two-factor data migration complete
config.x.otp_secret = ENV.fetch('OTP_SECRET', '1fc2b87989afa6351912abeebe31ffc5c476ead9bf8b3d74cbc4a302c7b69a45b40b1bbef3506ddad73e942e15ed5ca4b402bf9a66423626051104f4b5f05109') config.x.otp_secret = ENV.fetch('OTP_SECRET', '1fc2b87989afa6351912abeebe31ffc5c476ead9bf8b3d74cbc4a302c7b69a45b40b1bbef3506ddad73e942e15ed5ca4b402bf9a66423626051104f4b5f05109')
# Raise error when a before_action's only/except options reference missing actions # Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true config.action_controller.raise_on_missing_callback_actions = true
end end

View file

@ -23,9 +23,6 @@ Rails.application.configure do
# key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true # config.require_master_key = true
# Compress CSS using a preprocessor.
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed. # Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false config.assets.compile = false
@ -42,6 +39,7 @@ Rails.application.configure do
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true config.force_ssl = true
# Skip http-to-https redirect for the default health check endpoint.
config.ssl_options = { config.ssl_options = {
redirect: { redirect: {
exclude: ->(request) { request.path.start_with?('/health') || request.headers['Host'].end_with?('.onion') || request.headers['Host'].end_with?('.i2p') }, exclude: ->(request) { request.path.start_with?('/health') || request.headers['Host'].end_with?('.onion') || request.headers['Host'].end_with?('.i2p') },
@ -70,7 +68,7 @@ Rails.application.configure do
# config.action_mailer.raise_delivery_errors = false # config.action_mailer.raise_delivery_errors = false
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# English when a translation cannot be found). # the I18n.default_locale when a translation cannot be found).
# This setting would typically be `true` to use the `I18n.default_locale`. # This setting would typically be `true` to use the `I18n.default_locale`.
# Some locales are missing translation entries and would have errors: # Some locales are missing translation entries and would have errors:
# https://github.com/mastodon/mastodon/pull/24727 # https://github.com/mastodon/mastodon/pull/24727

View file

@ -26,7 +26,7 @@ Rails.application.configure do
config.action_controller.perform_caching = false config.action_controller.perform_caching = false
config.cache_store = :memory_store config.cache_store = :memory_store
# Raise exceptions instead of rendering exception templates. # Render exception templates for rescuable exceptions and raise for other exceptions.
config.action_dispatch.show_exceptions = :rescuable config.action_dispatch.show_exceptions = :rescuable
# Disable request forgery protection in test environment. # Disable request forgery protection in test environment.
@ -70,7 +70,7 @@ Rails.application.configure do
# Annotate rendered view with file names. # Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true # config.action_view.annotate_rendered_view_with_filenames = true
# Raise error when a before_action's only/except options reference missing actions # Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true config.action_controller.raise_on_missing_callback_actions = true
end end

View file

@ -348,7 +348,7 @@ namespace :api, format: false do
namespace :web do namespace :web do
resource :settings, only: [:update] resource :settings, only: [:update]
resources :embeds, only: [:show] resources :embeds, only: [:show]
resources :push_subscriptions, only: [:create] do resources :push_subscriptions, only: [:create, :destroy] do
member do member do
put :update put :update
end end

7
config/translation.yml Normal file
View file

@ -0,0 +1,7 @@
shared:
deepl:
api_key: <%= ENV.fetch('DEEPL_API_KEY', nil) %>
plan: <%= ENV.fetch('DEEPL_PLAN', 'free') %>
libre_translate:
api_key: <%= ENV.fetch('LIBRE_TRANSLATE_API_KEY', nil) %>
endpoint: <%= ENV.fetch('LIBRE_TRANSLATE_ENDPOINT', nil) %>

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
class RemoveDuplicateIndexes < ActiveRecord::Migration[7.1]
def change
remove_index :account_aliases, :account_id
remove_index :account_relationship_severance_events, :account_id
remove_index :custom_filter_statuses, :status_id
remove_index :webauthn_credentials, :user_id
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_10_07_071624) do ActiveRecord::Schema[7.1].define(version: 2024_10_14_010506) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -21,7 +21,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_07_071624) do
t.datetime "created_at", precision: nil, null: false t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false
t.index ["account_id", "uri"], name: "index_account_aliases_on_account_id_and_uri", unique: true t.index ["account_id", "uri"], name: "index_account_aliases_on_account_id_and_uri", unique: true
t.index ["account_id"], name: "index_account_aliases_on_account_id"
end end
create_table "account_conversations", force: :cascade do |t| create_table "account_conversations", force: :cascade do |t|
@ -99,7 +98,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_07_071624) do
t.integer "followers_count", default: 0, null: false t.integer "followers_count", default: 0, null: false
t.integer "following_count", default: 0, null: false t.integer "following_count", default: 0, null: false
t.index ["account_id", "relationship_severance_event_id"], name: "idx_on_account_id_relationship_severance_event_id_7bd82bf20e", unique: true t.index ["account_id", "relationship_severance_event_id"], name: "idx_on_account_id_relationship_severance_event_id_7bd82bf20e", unique: true
t.index ["account_id"], name: "index_account_relationship_severance_events_on_account_id"
t.index ["relationship_severance_event_id"], name: "idx_on_relationship_severance_event_id_403f53e707" t.index ["relationship_severance_event_id"], name: "idx_on_relationship_severance_event_id_403f53e707"
end end
@ -397,7 +395,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_07_071624) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["custom_filter_id"], name: "index_custom_filter_statuses_on_custom_filter_id" t.index ["custom_filter_id"], name: "index_custom_filter_statuses_on_custom_filter_id"
t.index ["status_id", "custom_filter_id"], name: "index_custom_filter_statuses_on_status_id_and_custom_filter_id", unique: true t.index ["status_id", "custom_filter_id"], name: "index_custom_filter_statuses_on_status_id_and_custom_filter_id", unique: true
t.index ["status_id"], name: "index_custom_filter_statuses_on_status_id"
end end
create_table "custom_filters", force: :cascade do |t| create_table "custom_filters", force: :cascade do |t|
@ -1205,7 +1202,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_07_071624) do
t.datetime "updated_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false
t.index ["external_id"], name: "index_webauthn_credentials_on_external_id", unique: true t.index ["external_id"], name: "index_webauthn_credentials_on_external_id", unique: true
t.index ["user_id", "nickname"], name: "index_webauthn_credentials_on_user_id_and_nickname", unique: true t.index ["user_id", "nickname"], name: "index_webauthn_credentials_on_user_id_and_nickname", unique: true
t.index ["user_id"], name: "index_webauthn_credentials_on_user_id"
end end
create_table "webhooks", force: :cascade do |t| create_table "webhooks", force: :cascade do |t|

View file

@ -1,7 +1,7 @@
{ {
"name": "@mastodon/mastodon", "name": "@mastodon/mastodon",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"packageManager": "yarn@4.5.0", "packageManager": "yarn@4.5.1",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@ -180,13 +180,13 @@
"eslint": "^8.41.0", "eslint": "^8.41.0",
"eslint-define-config": "^2.0.0", "eslint-define-config": "^2.0.0",
"eslint-import-resolver-typescript": "^3.5.5", "eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-formatjs": "^4.10.1", "eslint-plugin-formatjs": "^5.0.0",
"eslint-plugin-import": "~2.30.0", "eslint-plugin-import": "~2.30.0",
"eslint-plugin-jsdoc": "^50.0.0", "eslint-plugin-jsdoc": "^50.0.0",
"eslint-plugin-jsx-a11y": "~6.10.0", "eslint-plugin-jsx-a11y": "~6.10.0",
"eslint-plugin-promise": "~7.1.0", "eslint-plugin-promise": "~7.1.0",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^5.0.0",
"husky": "^9.0.11", "husky": "^9.0.11",
"jest": "^29.5.0", "jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0", "jest-environment-jsdom": "^29.5.0",

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
Fabricator(:account_conversation) do
account
conversation
status_ids { [Fabricate(:status).id] }
end

View file

@ -230,28 +230,6 @@ RSpec.describe ApplicationHelper do
end end
end end
describe 'visibility_icon' do
it 'returns a globe icon for a public visible status' do
result = helper.visibility_icon Status.new(visibility: 'public')
expect(result).to match(/globe/)
end
it 'returns an unlock icon for a unlisted visible status' do
result = helper.visibility_icon Status.new(visibility: 'unlisted')
expect(result).to match(/lock_open/)
end
it 'returns a lock icon for a private visible status' do
result = helper.visibility_icon Status.new(visibility: 'private')
expect(result).to match(/lock/)
end
it 'returns an at icon for a direct visible status' do
result = helper.visibility_icon Status.new(visibility: 'direct')
expect(result).to match(/alternate_email/)
end
end
describe 'title' do describe 'title' do
it 'returns site title on production environment' do it 'returns site title on production environment' do
Setting.site_title = 'site title' Setting.site_title = 'site title'

View file

@ -0,0 +1,61 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe DatabaseHelper do
context 'when a replica is enabled' do
around do |example|
ClimateControl.modify REPLICA_DB_NAME: 'prod-relay-quantum-tunnel-mirror' do
example.run
end
end
before { allow(ApplicationRecord).to receive(:connected_to) }
describe '#with_read_replica' do
it 'uses the replica for connections' do
helper.with_read_replica { _x = 1 }
expect(ApplicationRecord)
.to have_received(:connected_to).with(role: :reading, prevent_writes: true)
end
end
describe '#with_primary' do
it 'uses the primary for connections' do
helper.with_primary { _x = 1 }
expect(ApplicationRecord)
.to have_received(:connected_to).with(role: :writing)
end
end
end
context 'when a replica is not enabled' do
around do |example|
ClimateControl.modify REPLICA_DB_NAME: nil do
example.run
end
end
before { allow(ApplicationRecord).to receive(:connected_to) }
describe '#with_read_replica' do
it 'does not use the replica for connections' do
helper.with_read_replica { _x = 1 }
expect(ApplicationRecord)
.to_not have_received(:connected_to).with(role: :reading, prevent_writes: true)
end
end
describe '#with_primary' do
it 'does not use the primary for connections' do
helper.with_primary { _x = 1 }
expect(ApplicationRecord)
.to_not have_received(:connected_to).with(role: :writing)
end
end
end
end

View file

@ -3,6 +3,8 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe FeedManager do RSpec.describe FeedManager do
subject { described_class.instance }
before do |example| before do |example|
unless example.metadata[:skip_stub] unless example.metadata[:skip_stub]
stub_const 'FeedManager::MAX_ITEMS', 10 stub_const 'FeedManager::MAX_ITEMS', 10
@ -32,26 +34,26 @@ RSpec.describe FeedManager do
it 'returns false for followee\'s status' do it 'returns false for followee\'s status' do
status = Fabricate(:status, text: 'Hello world', account: alice) status = Fabricate(:status, text: 'Hello world', account: alice)
bob.follow!(alice) bob.follow!(alice)
expect(described_class.instance.filter?(:home, status, bob)).to be false expect(subject.filter?(:home, status, bob)).to be false
end end
it 'returns false for reblog by followee' do it 'returns false for reblog by followee' do
status = Fabricate(:status, text: 'Hello world', account: jeff) status = Fabricate(:status, text: 'Hello world', account: jeff)
reblog = Fabricate(:status, reblog: status, account: alice) reblog = Fabricate(:status, reblog: status, account: alice)
bob.follow!(alice) bob.follow!(alice)
expect(described_class.instance.filter?(:home, reblog, bob)).to be false expect(subject.filter?(:home, reblog, bob)).to be false
end end
it 'returns true for post from account who blocked me' do it 'returns true for post from account who blocked me' do
status = Fabricate(:status, text: 'Hello, World', account: alice) status = Fabricate(:status, text: 'Hello, World', account: alice)
alice.block!(bob) alice.block!(bob)
expect(described_class.instance.filter?(:home, status, bob)).to be true expect(subject.filter?(:home, status, bob)).to be true
end end
it 'returns true for post from blocked account' do it 'returns true for post from blocked account' do
status = Fabricate(:status, text: 'Hello, World', account: alice) status = Fabricate(:status, text: 'Hello, World', account: alice)
bob.block!(alice) bob.block!(alice)
expect(described_class.instance.filter?(:home, status, bob)).to be true expect(subject.filter?(:home, status, bob)).to be true
end end
it 'returns true for reblog by followee of blocked account' do it 'returns true for reblog by followee of blocked account' do
@ -59,7 +61,7 @@ RSpec.describe FeedManager do
reblog = Fabricate(:status, reblog: status, account: alice) reblog = Fabricate(:status, reblog: status, account: alice)
bob.follow!(alice) bob.follow!(alice)
bob.block!(jeff) bob.block!(jeff)
expect(described_class.instance.filter?(:home, reblog, bob)).to be true expect(subject.filter?(:home, reblog, bob)).to be true
end end
it 'returns true for reblog by followee of muted account' do it 'returns true for reblog by followee of muted account' do
@ -67,7 +69,7 @@ RSpec.describe FeedManager do
reblog = Fabricate(:status, reblog: status, account: alice) reblog = Fabricate(:status, reblog: status, account: alice)
bob.follow!(alice) bob.follow!(alice)
bob.mute!(jeff) bob.mute!(jeff)
expect(described_class.instance.filter?(:home, reblog, bob)).to be true expect(subject.filter?(:home, reblog, bob)).to be true
end end
it 'returns true for reblog by followee of someone who is blocking recipient' do it 'returns true for reblog by followee of someone who is blocking recipient' do
@ -75,14 +77,14 @@ RSpec.describe FeedManager do
reblog = Fabricate(:status, reblog: status, account: alice) reblog = Fabricate(:status, reblog: status, account: alice)
bob.follow!(alice) bob.follow!(alice)
jeff.block!(bob) jeff.block!(bob)
expect(described_class.instance.filter?(:home, reblog, bob)).to be true expect(subject.filter?(:home, reblog, bob)).to be true
end end
it 'returns true for reblog from account with reblogs disabled' do it 'returns true for reblog from account with reblogs disabled' do
status = Fabricate(:status, text: 'Hello world', account: jeff) status = Fabricate(:status, text: 'Hello world', account: jeff)
reblog = Fabricate(:status, reblog: status, account: alice) reblog = Fabricate(:status, reblog: status, account: alice)
bob.follow!(alice, reblogs: false) bob.follow!(alice, reblogs: false)
expect(described_class.instance.filter?(:home, reblog, bob)).to be true expect(subject.filter?(:home, reblog, bob)).to be true
end end
it 'returns false for reply by followee to another followee' do it 'returns false for reply by followee to another followee' do
@ -90,49 +92,49 @@ RSpec.describe FeedManager do
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
bob.follow!(alice) bob.follow!(alice)
bob.follow!(jeff) bob.follow!(jeff)
expect(described_class.instance.filter?(:home, reply, bob)).to be false expect(subject.filter?(:home, reply, bob)).to be false
end end
it 'returns false for reply by followee to recipient' do it 'returns false for reply by followee to recipient' do
status = Fabricate(:status, text: 'Hello world', account: bob) status = Fabricate(:status, text: 'Hello world', account: bob)
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
bob.follow!(alice) bob.follow!(alice)
expect(described_class.instance.filter?(:home, reply, bob)).to be false expect(subject.filter?(:home, reply, bob)).to be false
end end
it 'returns false for reply by followee to self' do it 'returns false for reply by followee to self' do
status = Fabricate(:status, text: 'Hello world', account: alice) status = Fabricate(:status, text: 'Hello world', account: alice)
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
bob.follow!(alice) bob.follow!(alice)
expect(described_class.instance.filter?(:home, reply, bob)).to be false expect(subject.filter?(:home, reply, bob)).to be false
end end
it 'returns true for reply by followee to non-followed account' do it 'returns true for reply by followee to non-followed account' do
status = Fabricate(:status, text: 'Hello world', account: jeff) status = Fabricate(:status, text: 'Hello world', account: jeff)
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
bob.follow!(alice) bob.follow!(alice)
expect(described_class.instance.filter?(:home, reply, bob)).to be true expect(subject.filter?(:home, reply, bob)).to be true
end end
it 'returns true for the second reply by followee to a non-federated status' do it 'returns true for the second reply by followee to a non-federated status' do
reply = Fabricate(:status, text: 'Reply 1', reply: true, account: alice) reply = Fabricate(:status, text: 'Reply 1', reply: true, account: alice)
second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice) second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice)
bob.follow!(alice) bob.follow!(alice)
expect(described_class.instance.filter?(:home, second_reply, bob)).to be true expect(subject.filter?(:home, second_reply, bob)).to be true
end end
it 'returns false for status by followee mentioning another account' do it 'returns false for status by followee mentioning another account' do
bob.follow!(alice) bob.follow!(alice)
jeff.follow!(alice) jeff.follow!(alice)
status = PostStatusService.new.call(alice, text: 'Hey @jeff') status = PostStatusService.new.call(alice, text: 'Hey @jeff')
expect(described_class.instance.filter?(:home, status, bob)).to be false expect(subject.filter?(:home, status, bob)).to be false
end end
it 'returns true for status by followee mentioning blocked account' do it 'returns true for status by followee mentioning blocked account' do
bob.block!(jeff) bob.block!(jeff)
bob.follow!(alice) bob.follow!(alice)
status = PostStatusService.new.call(alice, text: 'Hey @jeff') status = PostStatusService.new.call(alice, text: 'Hey @jeff')
expect(described_class.instance.filter?(:home, status, bob)).to be true expect(subject.filter?(:home, status, bob)).to be true
end end
it 'returns true for status by followee mentioning muted account' do it 'returns true for status by followee mentioning muted account' do
@ -147,19 +149,19 @@ RSpec.describe FeedManager do
alice.follow!(jeff) alice.follow!(jeff)
status = Fabricate(:status, text: 'Hello world', account: bob) status = Fabricate(:status, text: 'Hello world', account: bob)
reblog = Fabricate(:status, reblog: status, account: jeff) reblog = Fabricate(:status, reblog: status, account: jeff)
expect(described_class.instance.filter?(:home, reblog, alice)).to be true expect(subject.filter?(:home, reblog, alice)).to be true
end end
it 'returns true for German post when follow is set to English only' do it 'returns true for German post when follow is set to English only' do
alice.follow!(bob, languages: %w(en)) alice.follow!(bob, languages: %w(en))
status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de') status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
expect(described_class.instance.filter?(:home, status, alice)).to be true expect(subject.filter?(:home, status, alice)).to be true
end end
it 'returns false for German post when follow is set to German' do it 'returns false for German post when follow is set to German' do
alice.follow!(bob, languages: %w(de)) alice.follow!(bob, languages: %w(de))
status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de') status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
expect(described_class.instance.filter?(:home, status, alice)).to be false expect(subject.filter?(:home, status, alice)).to be false
end end
it 'returns true for post from followee on exclusive list' do it 'returns true for post from followee on exclusive list' do
@ -168,7 +170,7 @@ RSpec.describe FeedManager do
list.accounts << bob list.accounts << bob
allow(List).to receive(:where).and_return(list) allow(List).to receive(:where).and_return(list)
status = Fabricate(:status, text: 'I post a lot', account: bob) status = Fabricate(:status, text: 'I post a lot', account: bob)
expect(described_class.instance.filter?(:home, status, alice)).to be true expect(subject.filter?(:home, status, alice)).to be true
end end
it 'returns true for reblog from followee on exclusive list' do it 'returns true for reblog from followee on exclusive list' do
@ -178,7 +180,7 @@ RSpec.describe FeedManager do
allow(List).to receive(:where).and_return(list) allow(List).to receive(:where).and_return(list)
status = Fabricate(:status, text: 'I post a lot', account: bob) status = Fabricate(:status, text: 'I post a lot', account: bob)
reblog = Fabricate(:status, reblog: status, account: jeff) reblog = Fabricate(:status, reblog: status, account: jeff)
expect(described_class.instance.filter?(:home, reblog, alice)).to be true expect(subject.filter?(:home, reblog, alice)).to be true
end end
it 'returns false for post from followee on non-exclusive list' do it 'returns false for post from followee on non-exclusive list' do
@ -186,7 +188,7 @@ RSpec.describe FeedManager do
alice.follow!(bob) alice.follow!(bob)
list.accounts << bob list.accounts << bob
status = Fabricate(:status, text: 'I post a lot', account: bob) status = Fabricate(:status, text: 'I post a lot', account: bob)
expect(described_class.instance.filter?(:home, status, alice)).to be false expect(subject.filter?(:home, status, alice)).to be false
end end
it 'returns false for reblog from followee on non-exclusive list' do it 'returns false for reblog from followee on non-exclusive list' do
@ -195,7 +197,7 @@ RSpec.describe FeedManager do
list.accounts << jeff list.accounts << jeff
status = Fabricate(:status, text: 'I post a lot', account: bob) status = Fabricate(:status, text: 'I post a lot', account: bob)
reblog = Fabricate(:status, reblog: status, account: jeff) reblog = Fabricate(:status, reblog: status, account: jeff)
expect(described_class.instance.filter?(:home, reblog, alice)).to be false expect(subject.filter?(:home, reblog, alice)).to be false
end end
end end
@ -203,27 +205,27 @@ RSpec.describe FeedManager do
it 'returns true for status that mentions blocked account' do it 'returns true for status that mentions blocked account' do
bob.block!(jeff) bob.block!(jeff)
status = PostStatusService.new.call(alice, text: 'Hey @jeff') status = PostStatusService.new.call(alice, text: 'Hey @jeff')
expect(described_class.instance.filter?(:mentions, status, bob)).to be true expect(subject.filter?(:mentions, status, bob)).to be true
end end
it 'returns true for status that replies to a blocked account' do it 'returns true for status that replies to a blocked account' do
status = Fabricate(:status, text: 'Hello world', account: jeff) status = Fabricate(:status, text: 'Hello world', account: jeff)
reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
bob.block!(jeff) bob.block!(jeff)
expect(described_class.instance.filter?(:mentions, reply, bob)).to be true expect(subject.filter?(:mentions, reply, bob)).to be true
end end
it 'returns false for status by limited account who recipient is not following' do it 'returns false for status by limited account who recipient is not following' do
status = Fabricate(:status, text: 'Hello world', account: alice) status = Fabricate(:status, text: 'Hello world', account: alice)
alice.silence! alice.silence!
expect(described_class.instance.filter?(:mentions, status, bob)).to be false expect(subject.filter?(:mentions, status, bob)).to be false
end end
it 'returns false for status by followed limited account' do it 'returns false for status by followed limited account' do
status = Fabricate(:status, text: 'Hello world', account: alice) status = Fabricate(:status, text: 'Hello world', account: alice)
alice.silence! alice.silence!
bob.follow!(alice) bob.follow!(alice)
expect(described_class.instance.filter?(:mentions, status, bob)).to be false expect(subject.filter?(:mentions, status, bob)).to be false
end end
end end
end end
@ -235,7 +237,7 @@ RSpec.describe FeedManager do
members = Array.new(described_class::MAX_ITEMS) { |count| [count, count] } members = Array.new(described_class::MAX_ITEMS) { |count| [count, count] }
redis.zadd("feed:home:#{account.id}", members) redis.zadd("feed:home:#{account.id}", members)
described_class.instance.push_to_home(account, status) subject.push_to_home(account, status)
expect(redis.zcard("feed:home:#{account.id}")).to eq described_class::MAX_ITEMS expect(redis.zcard("feed:home:#{account.id}")).to eq described_class::MAX_ITEMS
end end
@ -246,7 +248,7 @@ RSpec.describe FeedManager do
reblogged = Fabricate(:status) reblogged = Fabricate(:status)
reblog = Fabricate(:status, reblog: reblogged) reblog = Fabricate(:status, reblog: reblogged)
expect(described_class.instance.push_to_home(account, reblog)).to be true expect(subject.push_to_home(account, reblog)).to be true
end end
it 'does not save a new reblog of a recent status' do it 'does not save a new reblog of a recent status' do
@ -254,9 +256,9 @@ RSpec.describe FeedManager do
reblogged = Fabricate(:status) reblogged = Fabricate(:status)
reblog = Fabricate(:status, reblog: reblogged) reblog = Fabricate(:status, reblog: reblogged)
described_class.instance.push_to_home(account, reblogged) subject.push_to_home(account, reblogged)
expect(described_class.instance.push_to_home(account, reblog)).to be false expect(subject.push_to_home(account, reblog)).to be false
end end
it 'saves a new reblog of an old status' do it 'saves a new reblog of an old status' do
@ -264,14 +266,14 @@ RSpec.describe FeedManager do
reblogged = Fabricate(:status) reblogged = Fabricate(:status)
reblog = Fabricate(:status, reblog: reblogged) reblog = Fabricate(:status, reblog: reblogged)
described_class.instance.push_to_home(account, reblogged) subject.push_to_home(account, reblogged)
# Fill the feed with intervening statuses # Fill the feed with intervening statuses
described_class::REBLOG_FALLOFF.times do described_class::REBLOG_FALLOFF.times do
described_class.instance.push_to_home(account, Fabricate(:status)) subject.push_to_home(account, Fabricate(:status))
end end
expect(described_class.instance.push_to_home(account, reblog)).to be true expect(subject.push_to_home(account, reblog)).to be true
end end
it 'does not save a new reblog of a recently-reblogged status' do it 'does not save a new reblog of a recently-reblogged status' do
@ -280,10 +282,10 @@ RSpec.describe FeedManager do
reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) } reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) }
# The first reblog will be accepted # The first reblog will be accepted
described_class.instance.push_to_home(account, reblogs.first) subject.push_to_home(account, reblogs.first)
# The second reblog should be ignored # The second reblog should be ignored
expect(described_class.instance.push_to_home(account, reblogs.last)).to be false expect(subject.push_to_home(account, reblogs.last)).to be false
end end
it 'saves a new reblog of a recently-reblogged status when previous reblog has been deleted' do it 'saves a new reblog of a recently-reblogged status when previous reblog has been deleted' do
@ -292,15 +294,15 @@ RSpec.describe FeedManager do
old_reblog = Fabricate(:status, reblog: reblogged) old_reblog = Fabricate(:status, reblog: reblogged)
# The first reblog should be accepted # The first reblog should be accepted
expect(described_class.instance.push_to_home(account, old_reblog)).to be true expect(subject.push_to_home(account, old_reblog)).to be true
# The first reblog should be successfully removed # The first reblog should be successfully removed
expect(described_class.instance.unpush_from_home(account, old_reblog)).to be true expect(subject.unpush_from_home(account, old_reblog)).to be true
reblog = Fabricate(:status, reblog: reblogged) reblog = Fabricate(:status, reblog: reblogged)
# The second reblog should be accepted # The second reblog should be accepted
expect(described_class.instance.push_to_home(account, reblog)).to be true expect(subject.push_to_home(account, reblog)).to be true
end end
it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do
@ -309,14 +311,14 @@ RSpec.describe FeedManager do
reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) } reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) }
# Accept the reblogs # Accept the reblogs
described_class.instance.push_to_home(account, reblogs[0]) subject.push_to_home(account, reblogs[0])
described_class.instance.push_to_home(account, reblogs[1]) subject.push_to_home(account, reblogs[1])
# Unreblog the first one # Unreblog the first one
described_class.instance.unpush_from_home(account, reblogs[0]) subject.unpush_from_home(account, reblogs[0])
# The last reblog should still be ignored # The last reblog should still be ignored
expect(described_class.instance.push_to_home(account, reblogs.last)).to be false expect(subject.push_to_home(account, reblogs.last)).to be false
end end
it 'saves a new reblog of a long-ago-reblogged status' do it 'saves a new reblog of a long-ago-reblogged status' do
@ -325,15 +327,15 @@ RSpec.describe FeedManager do
reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) } reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) }
# The first reblog will be accepted # The first reblog will be accepted
described_class.instance.push_to_home(account, reblogs.first) subject.push_to_home(account, reblogs.first)
# Fill the feed with intervening statuses # Fill the feed with intervening statuses
described_class::REBLOG_FALLOFF.times do described_class::REBLOG_FALLOFF.times do
described_class.instance.push_to_home(account, Fabricate(:status)) subject.push_to_home(account, Fabricate(:status))
end end
# The second reblog should also be accepted # The second reblog should also be accepted
expect(described_class.instance.push_to_home(account, reblogs.last)).to be true expect(subject.push_to_home(account, reblogs.last)).to be true
end end
end end
@ -341,9 +343,9 @@ RSpec.describe FeedManager do
account = Fabricate(:account) account = Fabricate(:account)
reblog = Fabricate(:status) reblog = Fabricate(:status)
status = Fabricate(:status, reblog: reblog) status = Fabricate(:status, reblog: reblog)
described_class.instance.push_to_home(account, status) subject.push_to_home(account, status)
expect(described_class.instance.push_to_home(account, reblog)).to be false expect(subject.push_to_home(account, reblog)).to be false
end end
end end
@ -366,9 +368,9 @@ RSpec.describe FeedManager do
it "does not push when the given status's reblog is already inserted" do it "does not push when the given status's reblog is already inserted" do
reblog = Fabricate(:status) reblog = Fabricate(:status)
status = Fabricate(:status, reblog: reblog) status = Fabricate(:status, reblog: reblog)
described_class.instance.push_to_list(list, status) subject.push_to_list(list, status)
expect(described_class.instance.push_to_list(list, reblog)).to be false expect(subject.push_to_list(list, reblog)).to be false
end end
context 'when replies policy is set to no replies' do context 'when replies policy is set to no replies' do
@ -378,19 +380,19 @@ RSpec.describe FeedManager do
it 'pushes statuses that are not replies' do it 'pushes statuses that are not replies' do
status = Fabricate(:status, text: 'Hello world', account: bob) status = Fabricate(:status, text: 'Hello world', account: bob)
expect(described_class.instance.push_to_list(list, status)).to be true expect(subject.push_to_list(list, status)).to be true
end end
it 'pushes statuses that are replies to list owner' do it 'pushes statuses that are replies to list owner' do
status = Fabricate(:status, text: 'Hello world', account: owner) status = Fabricate(:status, text: 'Hello world', account: owner)
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
expect(described_class.instance.push_to_list(list, reply)).to be true expect(subject.push_to_list(list, reply)).to be true
end end
it 'does not push replies to another member of the list' do it 'does not push replies to another member of the list' do
status = Fabricate(:status, text: 'Hello world', account: alice) status = Fabricate(:status, text: 'Hello world', account: alice)
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
expect(described_class.instance.push_to_list(list, reply)).to be false expect(subject.push_to_list(list, reply)).to be false
end end
end end
@ -401,25 +403,25 @@ RSpec.describe FeedManager do
it 'pushes statuses that are not replies' do it 'pushes statuses that are not replies' do
status = Fabricate(:status, text: 'Hello world', account: bob) status = Fabricate(:status, text: 'Hello world', account: bob)
expect(described_class.instance.push_to_list(list, status)).to be true expect(subject.push_to_list(list, status)).to be true
end end
it 'pushes statuses that are replies to list owner' do it 'pushes statuses that are replies to list owner' do
status = Fabricate(:status, text: 'Hello world', account: owner) status = Fabricate(:status, text: 'Hello world', account: owner)
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
expect(described_class.instance.push_to_list(list, reply)).to be true expect(subject.push_to_list(list, reply)).to be true
end end
it 'pushes replies to another member of the list' do it 'pushes replies to another member of the list' do
status = Fabricate(:status, text: 'Hello world', account: alice) status = Fabricate(:status, text: 'Hello world', account: alice)
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
expect(described_class.instance.push_to_list(list, reply)).to be true expect(subject.push_to_list(list, reply)).to be true
end end
it 'does not push replies to someone not a member of the list' do it 'does not push replies to someone not a member of the list' do
status = Fabricate(:status, text: 'Hello world', account: eve) status = Fabricate(:status, text: 'Hello world', account: eve)
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
expect(described_class.instance.push_to_list(list, reply)).to be false expect(subject.push_to_list(list, reply)).to be false
end end
end end
@ -430,25 +432,25 @@ RSpec.describe FeedManager do
it 'pushes statuses that are not replies' do it 'pushes statuses that are not replies' do
status = Fabricate(:status, text: 'Hello world', account: bob) status = Fabricate(:status, text: 'Hello world', account: bob)
expect(described_class.instance.push_to_list(list, status)).to be true expect(subject.push_to_list(list, status)).to be true
end end
it 'pushes statuses that are replies to list owner' do it 'pushes statuses that are replies to list owner' do
status = Fabricate(:status, text: 'Hello world', account: owner) status = Fabricate(:status, text: 'Hello world', account: owner)
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
expect(described_class.instance.push_to_list(list, reply)).to be true expect(subject.push_to_list(list, reply)).to be true
end end
it 'pushes replies to another member of the list' do it 'pushes replies to another member of the list' do
status = Fabricate(:status, text: 'Hello world', account: alice) status = Fabricate(:status, text: 'Hello world', account: alice)
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
expect(described_class.instance.push_to_list(list, reply)).to be true expect(subject.push_to_list(list, reply)).to be true
end end
it 'pushes replies to someone not a member of the list' do it 'pushes replies to someone not a member of the list' do
status = Fabricate(:status, text: 'Hello world', account: eve) status = Fabricate(:status, text: 'Hello world', account: eve)
reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
expect(described_class.instance.push_to_list(list, reply)).to be true expect(subject.push_to_list(list, reply)).to be true
end end
end end
end end
@ -458,9 +460,9 @@ RSpec.describe FeedManager do
account = Fabricate(:account, id: 0) account = Fabricate(:account, id: 0)
reblog = Fabricate(:status) reblog = Fabricate(:status)
status = Fabricate(:status, reblog: reblog) status = Fabricate(:status, reblog: reblog)
described_class.instance.push_to_home(account, status) subject.push_to_home(account, status)
described_class.instance.merge_into_home(account, reblog.account) subject.merge_into_home(account, reblog.account)
expect(redis.zscore('feed:home:0', reblog.id)).to be_nil expect(redis.zscore('feed:home:0', reblog.id)).to be_nil
end end
@ -473,14 +475,14 @@ RSpec.describe FeedManager do
reblogged = Fabricate(:status) reblogged = Fabricate(:status)
status = Fabricate(:status, reblog: reblogged) status = Fabricate(:status, reblog: reblogged)
described_class.instance.push_to_home(receiver, reblogged) subject.push_to_home(receiver, reblogged)
described_class::REBLOG_FALLOFF.times { described_class.instance.push_to_home(receiver, Fabricate(:status)) } described_class::REBLOG_FALLOFF.times { subject.push_to_home(receiver, Fabricate(:status)) }
described_class.instance.push_to_home(receiver, status) subject.push_to_home(receiver, status)
# The reblogging status should show up under normal conditions. # The reblogging status should show up under normal conditions.
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s) expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
described_class.instance.unpush_from_home(receiver, status) subject.unpush_from_home(receiver, status)
# Restore original status # Restore original status
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s) expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
@ -491,12 +493,12 @@ RSpec.describe FeedManager do
reblogged = Fabricate(:status) reblogged = Fabricate(:status)
status = Fabricate(:status, reblog: reblogged) status = Fabricate(:status, reblog: reblogged)
described_class.instance.push_to_home(receiver, status) subject.push_to_home(receiver, status)
# The reblogging status should show up under normal conditions. # The reblogging status should show up under normal conditions.
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [status.id.to_s] expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [status.id.to_s]
described_class.instance.unpush_from_home(receiver, status) subject.unpush_from_home(receiver, status)
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to be_empty expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to be_empty
end end
@ -506,14 +508,14 @@ RSpec.describe FeedManager do
reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) } reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) }
reblogs.each do |reblog| reblogs.each do |reblog|
described_class.instance.push_to_home(receiver, reblog) subject.push_to_home(receiver, reblog)
end end
# The reblogging status should show up under normal conditions. # The reblogging status should show up under normal conditions.
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s] expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s]
reblogs[0...-1].each do |reblog| reblogs[0...-1].each do |reblog|
described_class.instance.unpush_from_home(receiver, reblog) subject.unpush_from_home(receiver, reblog)
end end
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s] expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s]
@ -522,10 +524,10 @@ RSpec.describe FeedManager do
it 'sends push updates' do it 'sends push updates' do
status = Fabricate(:status) status = Fabricate(:status)
described_class.instance.push_to_home(receiver, status) subject.push_to_home(receiver, status)
allow(redis).to receive_messages(publish: nil) allow(redis).to receive_messages(publish: nil)
described_class.instance.unpush_from_home(receiver, status) subject.unpush_from_home(receiver, status)
deletion = Oj.dump(event: :delete, payload: status.id.to_s) deletion = Oj.dump(event: :delete, payload: status.id.to_s)
expect(redis).to have_received(:publish).with("timeline:#{receiver.id}", deletion) expect(redis).to have_received(:publish).with("timeline:#{receiver.id}", deletion)
@ -539,9 +541,9 @@ RSpec.describe FeedManager do
it 'leaves a tagged status' do it 'leaves a tagged status' do
status = Fabricate(:status) status = Fabricate(:status)
status.tags << tag status.tags << tag
described_class.instance.push_to_home(receiver, status) subject.push_to_home(receiver, status)
described_class.instance.unmerge_tag_from_home(tag, receiver) subject.unmerge_tag_from_home(tag, receiver)
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s) expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
end end
@ -552,9 +554,9 @@ RSpec.describe FeedManager do
status = Fabricate(:status, account: followee) status = Fabricate(:status, account: followee)
status.tags << tag status.tags << tag
described_class.instance.push_to_home(receiver, status) subject.push_to_home(receiver, status)
described_class.instance.unmerge_tag_from_home(tag, receiver) subject.unmerge_tag_from_home(tag, receiver)
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s) expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
end end
@ -562,9 +564,9 @@ RSpec.describe FeedManager do
it 'remains a tagged status written by receiver' do it 'remains a tagged status written by receiver' do
status = Fabricate(:status, account: receiver) status = Fabricate(:status, account: receiver)
status.tags << tag status.tags << tag
described_class.instance.push_to_home(receiver, status) subject.push_to_home(receiver, status)
described_class.instance.unmerge_tag_from_home(tag, receiver) subject.unmerge_tag_from_home(tag, receiver)
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s) expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
end end
@ -595,7 +597,7 @@ RSpec.describe FeedManager do
end end
it 'correctly cleans the home timeline' do it 'correctly cleans the home timeline' do
described_class.instance.clear_from_home(account, target_account) subject.clear_from_home(account, target_account)
expect(redis.zrange("feed:home:#{account.id}", 0, -1)).to eq [status_from_followed_account_first.id.to_s, status_from_followed_account_next.id.to_s] expect(redis.zrange("feed:home:#{account.id}", 0, -1)).to eq [status_from_followed_account_first.id.to_s, status_from_followed_account_next.id.to_s]
end end

View file

@ -8,4 +8,26 @@ RSpec.describe AccountAlias do
it { is_expected.to normalize(:acct).from(' @username@domain ').to('username@domain') } it { is_expected.to normalize(:acct).from(' @username@domain ').to('username@domain') }
end end
end end
describe 'Validations' do
subject { described_class.new(account:) }
let(:account) { Fabricate :account }
it { is_expected.to_not allow_values(nil, '').for(:uri).against(:acct).with_message(not_found_message) }
it { is_expected.to_not allow_values(account_uri).for(:uri).against(:acct).with_message(self_move_message) }
def account_uri
ActivityPub::TagManager.instance.uri_for(subject.account)
end
def not_found_message
I18n.t('migrations.errors.not_found')
end
def self_move_message
I18n.t('migrations.errors.move_to_self')
end
end
end end

View file

@ -9,8 +9,8 @@ RSpec.describe AccountMigration do
end end
end end
describe 'validations' do describe 'Validations' do
subject { described_class.new(account: source_account, acct: target_acct) } subject { Fabricate.build :account_migration, account: source_account }
let(:source_account) { Fabricate(:account) } let(:source_account) { Fabricate(:account) }
let(:target_acct) { target_account.acct } let(:target_acct) { target_account.acct }
@ -26,9 +26,7 @@ RSpec.describe AccountMigration do
allow(service_double).to receive(:call).with(target_acct, anything).and_return(target_account) allow(service_double).to receive(:call).with(target_acct, anything).and_return(target_account)
end end
it 'passes validations' do it { is_expected.to allow_value(target_account.acct).for(:acct) }
expect(subject).to be_valid
end
end end
context 'with unresolvable account' do context 'with unresolvable account' do
@ -40,17 +38,13 @@ RSpec.describe AccountMigration do
allow(service_double).to receive(:call).with(target_acct, anything).and_return(nil) allow(service_double).to receive(:call).with(target_acct, anything).and_return(nil)
end end
it 'has errors on acct field' do it { is_expected.to_not allow_value(target_acct).for(:acct) }
expect(subject).to model_have_error_on_field(:acct)
end
end end
context 'with a space in the domain part' do context 'with a space in the domain part' do
let(:target_acct) { 'target@remote. org' } let(:target_acct) { 'target@remote. org' }
it 'has errors on acct field' do it { is_expected.to_not allow_value(target_acct).for(:acct) }
expect(subject).to model_have_error_on_field(:acct)
end
end end
end end
end end

View file

@ -3,7 +3,8 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe AccountModerationNote do RSpec.describe AccountModerationNote do
describe 'chronological scope' do describe 'Scopes' do
describe '.chronological' do
it 'returns account moderation notes oldest to newest' do it 'returns account moderation notes oldest to newest' do
account = Fabricate(:account) account = Fabricate(:account)
note1 = Fabricate(:account_moderation_note, target_account: account) note1 = Fabricate(:account_moderation_note, target_account: account)
@ -12,20 +13,14 @@ RSpec.describe AccountModerationNote do
expect(account.targeted_moderation_notes.chronological).to eq [note1, note2] expect(account.targeted_moderation_notes.chronological).to eq [note1, note2]
end end
end end
describe 'validations' do
it 'is invalid if the content is empty' do
report = Fabricate.build(:account_moderation_note, content: '')
expect(report.valid?).to be false
end end
it 'is invalid if content is longer than character limit' do describe 'Validations' do
report = Fabricate.build(:account_moderation_note, content: comment_over_limit) subject { Fabricate.build :account_moderation_note }
expect(report.valid?).to be false
end
def comment_over_limit describe 'content' do
Faker::Lorem.paragraph_by_chars(number: described_class::CONTENT_SIZE_LIMIT * 2) it { is_expected.to_not allow_value('').for(:content) }
it { is_expected.to validate_length_of(:content).is_at_most(described_class::CONTENT_SIZE_LIMIT) }
end end
end end
end end

View file

@ -10,64 +10,6 @@ RSpec.describe Account do
let(:bob) { Fabricate(:account, username: 'bob') } let(:bob) { Fabricate(:account, username: 'bob') }
describe '#suspended_locally?' do
context 'when the account is not suspended' do
it 'returns false' do
expect(subject.suspended_locally?).to be false
end
end
context 'when the account is suspended locally' do
before do
subject.update!(suspended_at: 1.day.ago, suspension_origin: :local)
end
it 'returns true' do
expect(subject.suspended_locally?).to be true
end
end
context 'when the account is suspended remotely' do
before do
subject.update!(suspended_at: 1.day.ago, suspension_origin: :remote)
end
it 'returns false' do
expect(subject.suspended_locally?).to be false
end
end
end
describe '#suspend!' do
it 'marks the account as suspended and creates a deletion request' do
expect { subject.suspend! }
.to change(subject, :suspended?).from(false).to(true)
.and change(subject, :suspended_locally?).from(false).to(true)
.and(change { AccountDeletionRequest.exists?(account: subject) }.from(false).to(true))
end
context 'when the account is of a local user' do
subject { local_user_account }
let!(:local_user_account) { Fabricate(:user, email: 'foo+bar@domain.org').account }
it 'creates a canonical domain block' do
subject.suspend!
expect(CanonicalEmailBlock.block?(subject.user_email)).to be true
end
context 'when a canonical domain block already exists for that email' do
before do
Fabricate(:canonical_email_block, email: subject.user_email)
end
it 'does not raise an error' do
expect { subject.suspend! }.to_not raise_error
end
end
end
end
describe '#follow!' do describe '#follow!' do
it 'creates a follow' do it 'creates a follow' do
follow = subject.follow!(bob) follow = subject.follow!(bob)
@ -208,16 +150,16 @@ RSpec.describe Account do
end end
end end
context 'when last_webfingered_at is more than 24 hours before' do context 'when last_webfingered_at is before the threshold' do
let(:last_webfingered_at) { 25.hours.ago } let(:last_webfingered_at) { (described_class::STALE_THRESHOLD + 1.hour).ago }
it 'returns true' do it 'returns true' do
expect(account.possibly_stale?).to be true expect(account.possibly_stale?).to be true
end end
end end
context 'when last_webfingered_at is less than 24 hours before' do context 'when last_webfingered_at is after the threshold' do
let(:last_webfingered_at) { 23.hours.ago } let(:last_webfingered_at) { (described_class::STALE_THRESHOLD - 1.hour).ago }
it 'returns false' do it 'returns false' do
expect(account.possibly_stale?).to be false expect(account.possibly_stale?).to be false
@ -752,26 +694,42 @@ RSpec.describe Account do
end end
end end
describe '#prepare_contents' do describe 'Callbacks' do
subject { Fabricate.build :account, domain: domain, note: ' padded note ', display_name: ' padded name ' } describe 'Stripping content when required' do
context 'with a remote account' do
subject { Fabricate.build :account, domain: 'host.example', note: ' note ', display_name: ' display name ' }
context 'with local account' do it 'preserves content' do
let(:domain) { nil }
it 'strips values' do
expect { subject.valid? }
.to change(subject, :note).to('padded note')
.and(change(subject, :display_name).to('padded name'))
end
end
context 'with remote account' do
let(:domain) { 'host.example' }
it 'preserves values' do
expect { subject.valid? } expect { subject.valid? }
.to not_change(subject, :note) .to not_change(subject, :note)
.and(not_change(subject, :display_name)) .and not_change(subject, :display_name)
end
end
context 'with a local account' do
subject { Fabricate.build :account, domain: nil, note:, display_name: }
context 'with populated fields' do
let(:note) { ' note ' }
let(:display_name) { ' display name ' }
it 'strips content' do
expect { subject.valid? }
.to change(subject, :note).to('note')
.and change(subject, :display_name).to('display name')
end
end
context 'with empty fields' do
let(:note) { nil }
let(:display_name) { nil }
it 'preserves content' do
expect { subject.valid? }
.to not_change(subject, :note)
.and not_change(subject, :display_name)
end
end
end end
end end
end end
@ -826,22 +784,19 @@ RSpec.describe Account do
end end
end end
describe 'validations' do describe 'Validations' do
it { is_expected.to validate_presence_of(:username) } it { is_expected.to validate_presence_of(:username) }
context 'when is local' do context 'when account is local' do
it 'is invalid if the username is not unique in case-insensitive comparison among local accounts' do subject { Fabricate.build :account, domain: nil }
_account = Fabricate(:account, username: 'the_doctor')
non_unique_account = Fabricate.build(:account, username: 'the_Doctor') context 'with an existing differently-cased username account' do
non_unique_account.valid? before { Fabricate :account, username: 'the_doctor' }
expect(non_unique_account).to model_have_error_on_field(:username)
it { is_expected.to_not allow_value('the_Doctor').for(:username) }
end end
it 'is invalid if the username is reserved' do it { is_expected.to_not allow_value('support').for(:username) }
account = Fabricate.build(:account, username: 'support')
account.valid?
expect(account).to model_have_error_on_field(:username)
end
it 'is valid when username is reserved but record has already been created' do it 'is valid when username is reserved but record has already been created' do
account = Fabricate.build(:account, username: 'support') account = Fabricate.build(:account, username: 'support')
@ -849,9 +804,10 @@ RSpec.describe Account do
expect(account.valid?).to be true expect(account.valid?).to be true
end end
it 'is valid if we are creating an instance actor account with a period' do context 'with the instance actor' do
account = Fabricate.build(:account, id: described_class::INSTANCE_ACTOR_ID, actor_type: 'Application', locked: true, username: 'example.com') subject { Fabricate.build :account, id: described_class::INSTANCE_ACTOR_ID, actor_type: 'Application', locked: true }
expect(account.valid?).to be true
it { is_expected.to allow_value('example.com').for(:username) }
end end
it 'is valid if we are creating a possibly-conflicting instance actor account' do it 'is valid if we are creating a possibly-conflicting instance actor account' do
@ -860,81 +816,31 @@ RSpec.describe Account do
expect(instance_account.valid?).to be true expect(instance_account.valid?).to be true
end end
it 'is invalid if the username doesn\'t only contains letters, numbers and underscores' do it { is_expected.to_not allow_values('the-doctor', 'the.doctor').for(:username) }
account = Fabricate.build(:account, username: 'the-doctor')
account.valid? it { is_expected.to validate_length_of(:username).is_at_most(described_class::USERNAME_LENGTH_LIMIT) }
expect(account).to model_have_error_on_field(:username) it { is_expected.to validate_length_of(:display_name).is_at_most(described_class::DISPLAY_NAME_LENGTH_LIMIT) }
it { is_expected.to_not allow_values(account_note_over_limit).for(:note) }
end end
it 'is invalid if the username contains a period' do context 'when account is remote' do
account = Fabricate.build(:account, username: 'the.doctor') subject { Fabricate.build :account, domain: 'host.example' }
account.valid?
expect(account).to model_have_error_on_field(:username) context 'when a normalized domain account exists' do
subject { Fabricate.build :account, domain: 'xn--r9j5b5b' }
before { Fabricate(:account, domain: 'にゃん', username: 'username') }
it { is_expected.to_not allow_values('username', 'Username').for(:username) }
end end
it 'is invalid if the username is longer than the character limit' do it { is_expected.to allow_values('the-doctor', username_over_limit).for(:username) }
account = Fabricate.build(:account, username: username_over_limit) it { is_expected.to_not allow_values('the doctor').for(:username) }
account.valid?
expect(account).to model_have_error_on_field(:username)
end
it 'is invalid if the display name is longer than the character limit' do it { is_expected.to allow_values(display_name_over_limit).for(:display_name) }
account = Fabricate.build(:account, display_name: display_name_over_limit)
account.valid?
expect(account).to model_have_error_on_field(:display_name)
end
it 'is invalid if the note is longer than the character limit' do it { is_expected.to allow_values(account_note_over_limit).for(:note) }
account = Fabricate.build(:account, note: account_note_over_limit)
account.valid?
expect(account).to model_have_error_on_field(:note)
end
end
context 'when is remote' do
it 'is invalid if the username is same among accounts in the same normalized domain' do
Fabricate(:account, domain: 'にゃん', username: 'username')
account = Fabricate.build(:account, domain: 'xn--r9j5b5b', username: 'username')
account.valid?
expect(account).to model_have_error_on_field(:username)
end
it 'is invalid if the username is not unique in case-insensitive comparison among accounts in the same normalized domain' do
Fabricate(:account, domain: 'にゃん', username: 'username')
account = Fabricate.build(:account, domain: 'xn--r9j5b5b', username: 'Username')
account.valid?
expect(account).to model_have_error_on_field(:username)
end
it 'is valid even if the username contains hyphens' do
account = Fabricate.build(:account, domain: 'domain', username: 'the-doctor')
account.valid?
expect(account).to_not model_have_error_on_field(:username)
end
it 'is invalid if the username doesn\'t only contains letters, numbers, underscores and hyphens' do
account = Fabricate.build(:account, domain: 'domain', username: 'the doctor')
account.valid?
expect(account).to model_have_error_on_field(:username)
end
it 'is valid even if the username is longer than the character limit' do
account = Fabricate.build(:account, domain: 'domain', username: username_over_limit)
account.valid?
expect(account).to_not model_have_error_on_field(:username)
end
it 'is valid even if the display name is longer than the character limit' do
account = Fabricate.build(:account, domain: 'domain', display_name: display_name_over_limit)
account.valid?
expect(account).to_not model_have_error_on_field(:display_name)
end
it 'is valid even if the note is longer than the character limit' do
account = Fabricate.build(:account, domain: 'domain', note: account_note_over_limit)
account.valid?
expect(account).to_not model_have_error_on_field(:note)
end
end end
def username_over_limit def username_over_limit
@ -1085,14 +991,6 @@ RSpec.describe Account do
end end
end end
describe 'suspended' do
it 'returns an array of accounts who are suspended' do
suspended_account = Fabricate(:account, suspended: true)
_account = Fabricate(:account, suspended: false)
expect(described_class.suspended).to contain_exactly(suspended_account)
end
end
describe 'searchable' do describe 'searchable' do
let!(:suspended_local) { Fabricate(:account, suspended: true, username: 'suspended_local') } let!(:suspended_local) { Fabricate(:account, suspended: true, username: 'suspended_local') }
let!(:suspended_remote) { Fabricate(:account, suspended: true, domain: 'example.org', username: 'suspended_remote') } let!(:suspended_remote) { Fabricate(:account, suspended: true, domain: 'example.org', username: 'suspended_remote') }

View file

@ -5,13 +5,12 @@ require 'rails_helper'
RSpec.describe AccountStatusesCleanupPolicy do RSpec.describe AccountStatusesCleanupPolicy do
let(:account) { Fabricate(:account, username: 'alice', domain: nil) } let(:account) { Fabricate(:account, username: 'alice', domain: nil) }
describe 'validation' do describe 'Validations' do
it 'disallow remote accounts' do subject { Fabricate.build :account_statuses_cleanup_policy }
account.update(domain: 'example.com')
account_statuses_cleanup_policy = Fabricate.build(:account_statuses_cleanup_policy, account: account) let(:remote_account) { Fabricate(:account, domain: 'example.com') }
account_statuses_cleanup_policy.valid?
expect(account_statuses_cleanup_policy).to model_have_error_on_field(:account) it { is_expected.to_not allow_value(remote_account).for(:account) }
end
end end
describe 'save hooks' do describe 'save hooks' do
@ -339,14 +338,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
end end
context 'when policy is set to keep DMs and reject everything else' do context 'when policy is set to keep DMs and reject everything else' do
before do before { establish_policy(keep_direct: true) }
account_statuses_cleanup_policy.keep_direct = true
account_statuses_cleanup_policy.keep_pinned = false
account_statuses_cleanup_policy.keep_polls = false
account_statuses_cleanup_policy.keep_media = false
account_statuses_cleanup_policy.keep_self_fav = false
account_statuses_cleanup_policy.keep_self_bookmark = false
end
it 'returns every old status except does not return the old direct message for deletion' do it 'returns every old status except does not return the old direct message for deletion' do
expect(subject.pluck(:id)) expect(subject.pluck(:id))
@ -356,14 +348,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
end end
context 'when policy is set to keep self-bookmarked toots and reject everything else' do context 'when policy is set to keep self-bookmarked toots and reject everything else' do
before do before { establish_policy(keep_self_bookmark: true) }
account_statuses_cleanup_policy.keep_direct = false
account_statuses_cleanup_policy.keep_pinned = false
account_statuses_cleanup_policy.keep_polls = false
account_statuses_cleanup_policy.keep_media = false
account_statuses_cleanup_policy.keep_self_fav = false
account_statuses_cleanup_policy.keep_self_bookmark = true
end
it 'returns every old status but does not return the old self-bookmarked message for deletion' do it 'returns every old status but does not return the old self-bookmarked message for deletion' do
expect(subject.pluck(:id)) expect(subject.pluck(:id))
@ -373,14 +358,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
end end
context 'when policy is set to keep self-faved toots and reject everything else' do context 'when policy is set to keep self-faved toots and reject everything else' do
before do before { establish_policy(keep_self_fav: true) }
account_statuses_cleanup_policy.keep_direct = false
account_statuses_cleanup_policy.keep_pinned = false
account_statuses_cleanup_policy.keep_polls = false
account_statuses_cleanup_policy.keep_media = false
account_statuses_cleanup_policy.keep_self_fav = true
account_statuses_cleanup_policy.keep_self_bookmark = false
end
it 'returns every old status but does not return the old self-faved message for deletion' do it 'returns every old status but does not return the old self-faved message for deletion' do
expect(subject.pluck(:id)) expect(subject.pluck(:id))
@ -390,14 +368,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
end end
context 'when policy is set to keep toots with media and reject everything else' do context 'when policy is set to keep toots with media and reject everything else' do
before do before { establish_policy(keep_media: true) }
account_statuses_cleanup_policy.keep_direct = false
account_statuses_cleanup_policy.keep_pinned = false
account_statuses_cleanup_policy.keep_polls = false
account_statuses_cleanup_policy.keep_media = true
account_statuses_cleanup_policy.keep_self_fav = false
account_statuses_cleanup_policy.keep_self_bookmark = false
end
it 'returns every old status but does not return the old message with media for deletion' do it 'returns every old status but does not return the old message with media for deletion' do
expect(subject.pluck(:id)) expect(subject.pluck(:id))
@ -407,14 +378,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
end end
context 'when policy is set to keep toots with polls and reject everything else' do context 'when policy is set to keep toots with polls and reject everything else' do
before do before { establish_policy(keep_polls: true) }
account_statuses_cleanup_policy.keep_direct = false
account_statuses_cleanup_policy.keep_pinned = false
account_statuses_cleanup_policy.keep_polls = true
account_statuses_cleanup_policy.keep_media = false
account_statuses_cleanup_policy.keep_self_fav = false
account_statuses_cleanup_policy.keep_self_bookmark = false
end
it 'returns every old status but does not return the old poll message for deletion' do it 'returns every old status but does not return the old poll message for deletion' do
expect(subject.pluck(:id)) expect(subject.pluck(:id))
@ -424,14 +388,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
end end
context 'when policy is set to keep pinned toots and reject everything else' do context 'when policy is set to keep pinned toots and reject everything else' do
before do before { establish_policy(keep_pinned: true) }
account_statuses_cleanup_policy.keep_direct = false
account_statuses_cleanup_policy.keep_pinned = true
account_statuses_cleanup_policy.keep_polls = false
account_statuses_cleanup_policy.keep_media = false
account_statuses_cleanup_policy.keep_self_fav = false
account_statuses_cleanup_policy.keep_self_bookmark = false
end
it 'returns every old status but does not return the old pinned message for deletion' do it 'returns every old status but does not return the old pinned message for deletion' do
expect(subject.pluck(:id)) expect(subject.pluck(:id))
@ -441,14 +398,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
end end
context 'when policy is to not keep any special messages' do context 'when policy is to not keep any special messages' do
before do before { establish_policy }
account_statuses_cleanup_policy.keep_direct = false
account_statuses_cleanup_policy.keep_pinned = false
account_statuses_cleanup_policy.keep_polls = false
account_statuses_cleanup_policy.keep_media = false
account_statuses_cleanup_policy.keep_self_fav = false
account_statuses_cleanup_policy.keep_self_bookmark = false
end
it 'returns every old status but does not return the recent or unrelated statuses' do it 'returns every old status but does not return the recent or unrelated statuses' do
expect(subject.pluck(:id)) expect(subject.pluck(:id))
@ -459,14 +409,7 @@ RSpec.describe AccountStatusesCleanupPolicy do
end end
context 'when policy is set to keep every category of toots' do context 'when policy is set to keep every category of toots' do
before do before { establish_policy(keep_direct: true, keep_pinned: true, keep_polls: true, keep_media: true, keep_self_fav: true, keep_self_bookmark: true) }
account_statuses_cleanup_policy.keep_direct = true
account_statuses_cleanup_policy.keep_pinned = true
account_statuses_cleanup_policy.keep_polls = true
account_statuses_cleanup_policy.keep_media = true
account_statuses_cleanup_policy.keep_self_fav = true
account_statuses_cleanup_policy.keep_self_bookmark = true
end
it 'returns normal statuses and does not return unrelated old status' do it 'returns normal statuses and does not return unrelated old status' do
expect(subject.pluck(:id)) expect(subject.pluck(:id))
@ -502,5 +445,24 @@ RSpec.describe AccountStatusesCleanupPolicy do
.and include(very_old_status.id, faved_primary.id, reblogged_primary.id, reblogged_secondary.id) .and include(very_old_status.id, faved_primary.id, reblogged_primary.id, reblogged_secondary.id)
end end
end end
private
def establish_policy(options = {})
default_policy_options.merge(options).each do |attribute, value|
account_statuses_cleanup_policy.send :"#{attribute}=", value
end
end
def default_policy_options
{
keep_direct: false,
keep_media: false,
keep_pinned: false,
keep_polls: false,
keep_self_bookmark: false,
keep_self_fav: false,
}
end
end end
end end

View file

@ -67,18 +67,30 @@ RSpec.describe Announcement do
it { is_expected.to validate_presence_of(:text) } it { is_expected.to validate_presence_of(:text) }
describe 'ends_at' do describe 'ends_at' do
it 'validates presence when starts_at is present' do context 'when starts_at is present' do
record = Fabricate.build(:announcement, starts_at: 1.day.ago) subject { Fabricate.build :announcement, starts_at: 1.day.ago }
expect(record).to_not be_valid it { is_expected.to validate_presence_of(:ends_at) }
expect(record.errors[:ends_at]).to be_present
end end
it 'does not validate presence when starts_at is missing' do context 'when starts_at is missing' do
record = Fabricate.build(:announcement, starts_at: nil) subject { Fabricate.build :announcement, starts_at: nil }
expect(record).to be_valid it { is_expected.to_not validate_presence_of(:ends_at) }
expect(record.errors[:ends_at]).to_not be_present end
end
describe 'starts_at' do
context 'when ends_at is present' do
subject { Fabricate.build :announcement, ends_at: 1.day.ago }
it { is_expected.to validate_presence_of(:starts_at) }
end
context 'when ends_at is missing' do
subject { Fabricate.build :announcement, ends_at: nil }
it { is_expected.to_not validate_presence_of(:starts_at) }
end end
end end
end end

View file

@ -4,20 +4,85 @@ require 'rails_helper'
RSpec.describe Appeal do RSpec.describe Appeal do
describe 'Validations' do describe 'Validations' do
it 'validates text length is under limit' do subject { Fabricate.build :appeal, strike: Fabricate(:account_warning) }
appeal = Fabricate.build(
:appeal,
strike: Fabricate(:account_warning),
text: 'a' * described_class::TEXT_LENGTH_LIMIT * 2
)
expect(appeal).to_not be_valid it { is_expected.to validate_length_of(:text).is_at_most(described_class::TEXT_LENGTH_LIMIT) }
expect(appeal).to model_have_error_on_field(:text)
context 'with a strike created too long ago' do
let(:strike) { Fabricate.build :account_warning, created_at: 100.days.ago }
it { is_expected.to_not allow_values(strike).for(:strike).against(:base).on(:create) }
end end
end end
describe 'scopes' do describe 'Query methods' do
describe 'approved' do describe '#pending?' do
subject { Fabricate.build :appeal, approved_at:, rejected_at: }
context 'with not approved and not rejected' do
let(:approved_at) { nil }
let(:rejected_at) { nil }
it { expect(subject).to be_pending }
end
context 'with approved and rejected' do
let(:approved_at) { 1.day.ago }
let(:rejected_at) { 1.day.ago }
it { expect(subject).to_not be_pending }
end
context 'with approved and not rejected' do
let(:approved_at) { 1.day.ago }
let(:rejected_at) { nil }
it { expect(subject).to_not be_pending }
end
context 'with not approved and rejected' do
let(:approved_at) { nil }
let(:rejected_at) { 1.day.ago }
it { expect(subject).to_not be_pending }
end
end
describe '#approved?' do
subject { Fabricate.build :appeal, approved_at: }
context 'with not approved' do
let(:approved_at) { nil }
it { expect(subject).to_not be_approved }
end
context 'with approved' do
let(:approved_at) { 1.day.ago }
it { expect(subject).to be_approved }
end
end
describe '#rejected?' do
subject { Fabricate.build :appeal, rejected_at: }
context 'with not rejected' do
let(:rejected_at) { nil }
it { expect(subject).to_not be_rejected }
end
context 'with rejected' do
let(:rejected_at) { 1.day.ago }
it { expect(subject).to be_rejected }
end
end
end
describe 'Scopes' do
describe '.approved' do
let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) } let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) }
let(:not_approved_appeal) { Fabricate(:appeal, approved_at: nil) } let(:not_approved_appeal) { Fabricate(:appeal, approved_at: nil) }
@ -27,7 +92,7 @@ RSpec.describe Appeal do
end end
end end
describe 'rejected' do describe '.rejected' do
let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) } let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) }
let(:not_rejected_appeal) { Fabricate(:appeal, rejected_at: nil) } let(:not_rejected_appeal) { Fabricate(:appeal, rejected_at: nil) }
@ -37,7 +102,7 @@ RSpec.describe Appeal do
end end
end end
describe 'pending' do describe '.pending' do
let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) } let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) }
let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) } let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) }
let(:pending_appeal) { Fabricate(:appeal, rejected_at: nil, approved_at: nil) } let(:pending_appeal) { Fabricate(:appeal, rejected_at: nil, approved_at: nil) }

View file

@ -0,0 +1,65 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Account::Suspensions do
subject { Fabricate(:account) }
describe '.suspended' do
let!(:suspended_account) { Fabricate :account, suspended: true }
before { Fabricate :account, suspended: false }
it 'returns accounts that are suspended' do
expect(Account.suspended)
.to contain_exactly(suspended_account)
end
end
describe '#suspended_locally?' do
context 'when the account is not suspended' do
it { is_expected.to_not be_suspended_locally }
end
context 'when the account is suspended locally' do
before { subject.update!(suspended_at: 1.day.ago, suspension_origin: :local) }
it { is_expected.to be_suspended_locally }
end
context 'when the account is suspended remotely' do
before { subject.update!(suspended_at: 1.day.ago, suspension_origin: :remote) }
it { is_expected.to_not be_suspended_locally }
end
end
describe '#suspend!' do
it 'marks the account as suspended and creates a deletion request' do
expect { subject.suspend! }
.to change(subject, :suspended?).from(false).to(true)
.and change(subject, :suspended_locally?).from(false).to(true)
.and(change { AccountDeletionRequest.exists?(account: subject) }.from(false).to(true))
end
context 'when the account is of a local user' do
subject { local_user_account }
let!(:local_user_account) { Fabricate(:user, email: 'foo+bar@domain.org').account }
it 'creates a canonical domain block' do
expect { subject.suspend! }
.to change { CanonicalEmailBlock.block?(subject.user_email) }.from(false).to(true)
end
context 'when a canonical domain block already exists for that email' do
before { Fabricate(:canonical_email_block, email: subject.user_email) }
it 'does not raise an error' do
expect { subject.suspend! }
.to_not raise_error
end
end
end
end
end

View file

@ -6,11 +6,10 @@ RSpec.describe DomainAllow do
describe 'Validations' do describe 'Validations' do
it { is_expected.to validate_presence_of(:domain) } it { is_expected.to validate_presence_of(:domain) }
it 'is invalid if the same normalized domain already exists' do context 'when a normalized domain exists' do
_domain_allow = Fabricate(:domain_allow, domain: 'にゃん') before { Fabricate(:domain_allow, domain: 'にゃん') }
domain_allow_with_normalized_value = Fabricate.build(:domain_allow, domain: 'xn--r9j5b5b')
domain_allow_with_normalized_value.valid? it { is_expected.to_not allow_value('xn--r9j5b5b').for(:domain) }
expect(domain_allow_with_normalized_value).to model_have_error_on_field(:domain)
end end
end end
end end

View file

@ -3,27 +3,26 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Follow do RSpec.describe Follow do
let(:alice) { Fabricate(:account, username: 'alice') } describe 'Associations' do
let(:bob) { Fabricate(:account, username: 'bob') }
describe 'validations' do
subject { described_class.new(account: alice, target_account: bob, rate_limit: true) }
it { is_expected.to belong_to(:account).required } it { is_expected.to belong_to(:account).required }
it { is_expected.to belong_to(:target_account).required } it { is_expected.to belong_to(:target_account).required }
it 'is invalid if account already follows too many people' do
alice.update(following_count: FollowLimitValidator::LIMIT)
expect(subject).to_not be_valid
expect(subject).to model_have_error_on_field(:base)
end end
it 'is valid if account is only on the brink of following too many people' do describe 'Validations' do
alice.update(following_count: FollowLimitValidator::LIMIT - 1) subject { Fabricate.build :follow, rate_limit: true }
expect(subject).to be_valid let(:account) { Fabricate(:account) }
expect(subject).to_not model_have_error_on_field(:base)
context 'when account follows too many people' do
before { account.update(following_count: FollowLimitValidator::LIMIT) }
it { is_expected.to_not allow_value(account).for(:account).against(:base) }
end
context 'when account is on brink of following too many people' do
before { account.update(following_count: FollowLimitValidator::LIMIT - 1) }
it { is_expected.to allow_value(account).for(:account).against(:base) }
end end
end end
@ -54,4 +53,58 @@ RSpec.describe Follow do
expect(account.requested?(target_account)).to be true expect(account.requested?(target_account)).to be true
end end
end end
describe '#local?' do
it { is_expected.to_not be_local }
end
describe 'Callbacks' do
describe 'Setting a URI' do
context 'when URI exists' do
subject { Fabricate.build :follow, uri: 'https://uri/value' }
it 'does not change' do
expect { subject.save }
.to not_change(subject, :uri)
end
end
context 'when URI is blank' do
subject { Fabricate.build :follow, uri: nil }
it 'populates the value' do
expect { subject.save }
.to change(subject, :uri).to(be_present)
end
end
end
describe 'Maintaining counters' do
subject { Fabricate.build :follow, account:, target_account: }
let(:account) { Fabricate :account }
let(:target_account) { Fabricate :account }
before do
account.account_stat.update following_count: 123
target_account.account_stat.update followers_count: 123
end
describe 'saving the follow' do
it 'increments counters' do
expect { subject.save }
.to change(account, :following_count).by(1)
.and(change(target_account, :followers_count).by(1))
end
end
describe 'destroying the follow' do
it 'decrements counters' do
expect { subject.destroy }
.to change(account, :following_count).by(-1)
.and(change(target_account, :followers_count).by(-1))
end
end
end
end
end end

View file

@ -3,33 +3,17 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Form::AdminSettings do RSpec.describe Form::AdminSettings do
describe 'validations' do describe 'Validations' do
describe 'site_contact_username' do describe 'site_contact_username' do
context 'with no accounts' do context 'with no accounts' do
it 'is not valid' do it { is_expected.to_not allow_value('Test').for(:site_contact_username) }
setting = described_class.new(site_contact_username: 'Test')
setting.valid?
expect(setting).to model_have_error_on_field(:site_contact_username)
end
end end
context 'with an account' do context 'with an account' do
before { Fabricate(:account, username: 'Glorp') } before { Fabricate(:account, username: 'Glorp') }
it 'is not valid when account doesnt match' do it { is_expected.to_not allow_value('Test').for(:site_contact_username) }
setting = described_class.new(site_contact_username: 'Test') it { is_expected.to allow_value('Glorp').for(:site_contact_username) }
setting.valid?
expect(setting).to model_have_error_on_field(:site_contact_username)
end
it 'is valid when account matches' do
setting = described_class.new(site_contact_username: 'Glorp')
setting.valid?
expect(setting).to_not model_have_error_on_field(:site_contact_username)
end
end end
end end
end end

View file

@ -3,18 +3,13 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe IpBlock do RSpec.describe IpBlock do
describe 'validations' do describe 'Validations' do
subject { Fabricate.build :ip_block }
it { is_expected.to validate_presence_of(:ip) } it { is_expected.to validate_presence_of(:ip) }
it { is_expected.to validate_presence_of(:severity) } it { is_expected.to validate_presence_of(:severity) }
it 'validates ip uniqueness', :aggregate_failures do it { is_expected.to validate_uniqueness_of(:ip) }
described_class.create!(ip: '127.0.0.1', severity: :no_access)
ip_block = described_class.new(ip: '127.0.0.1', severity: :no_access)
expect(ip_block).to_not be_valid
expect(ip_block).to model_have_error_on_field(:ip)
end
end end
describe '#to_log_human_identifier' do describe '#to_log_human_identifier' do

View file

@ -9,26 +9,10 @@ RSpec.describe PreviewCard do
end end
end end
describe 'validations' do describe 'Validations' do
describe 'urls' do describe 'url' do
it 'allows http schemes' do it { is_expected.to allow_values('http://example.host/path', 'https://example.host/path').for(:url) }
record = described_class.new(url: 'http://example.host/path') it { is_expected.to_not allow_value('javascript:alert()').for(:url) }
expect(record).to be_valid
end
it 'allows https schemes' do
record = described_class.new(url: 'https://example.host/path')
expect(record).to be_valid
end
it 'does not allow javascript: schemes' do
record = described_class.new(url: 'javascript:alert()')
expect(record).to_not be_valid
expect(record).to model_have_error_on_field(:url)
end
end end
end end
end end

View file

@ -3,7 +3,8 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe ReportNote do RSpec.describe ReportNote do
describe 'chronological scope' do describe 'Scopes' do
describe '.chronological' do
it 'returns report notes oldest to newest' do it 'returns report notes oldest to newest' do
report = Fabricate(:report) report = Fabricate(:report)
note1 = Fabricate(:report_note, report: report) note1 = Fabricate(:report_note, report: report)
@ -12,20 +13,14 @@ RSpec.describe ReportNote do
expect(report.notes.chronological).to eq [note1, note2] expect(report.notes.chronological).to eq [note1, note2]
end end
end end
describe 'validations' do
it 'is invalid if the content is empty' do
report = Fabricate.build(:report_note, content: '')
expect(report.valid?).to be false
end end
it 'is invalid if content is longer than character limit' do describe 'Validations' do
report = Fabricate.build(:report_note, content: comment_over_limit) subject { Fabricate.build :report_note }
expect(report.valid?).to be false
end
def comment_over_limit describe 'content' do
Faker::Lorem.paragraph_by_chars(number: described_class::CONTENT_SIZE_LIMIT * 2) it { is_expected.to_not allow_value('').for(:content) }
it { is_expected.to validate_length_of(:content).is_at_most(described_class::CONTENT_SIZE_LIMIT) }
end end
end end
end end

View file

@ -3,53 +3,17 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe WebauthnCredential do RSpec.describe WebauthnCredential do
describe 'validations' do describe 'Validations' do
subject { Fabricate.build :webauthn_credential }
it { is_expected.to validate_presence_of(:external_id) } it { is_expected.to validate_presence_of(:external_id) }
it { is_expected.to validate_presence_of(:public_key) } it { is_expected.to validate_presence_of(:public_key) }
it { is_expected.to validate_presence_of(:nickname) } it { is_expected.to validate_presence_of(:nickname) }
it { is_expected.to validate_presence_of(:sign_count) } it { is_expected.to validate_presence_of(:sign_count) }
it 'is invalid if already exist a webauthn credential with the same external id' do it { is_expected.to validate_uniqueness_of(:external_id) }
Fabricate(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw') it { is_expected.to validate_uniqueness_of(:nickname).scoped_to(:user_id) }
new_webauthn_credential = Fabricate.build(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw')
new_webauthn_credential.valid? it { is_expected.to validate_numericality_of(:sign_count).only_integer.is_greater_than_or_equal_to(0).is_less_than_or_equal_to(described_class::SIGN_COUNT_LIMIT - 1) }
expect(new_webauthn_credential).to model_have_error_on_field(:external_id)
end
it 'is invalid if user already registered a webauthn credential with the same nickname' do
user = Fabricate(:user)
Fabricate(:webauthn_credential, user_id: user.id, nickname: 'USB Key')
new_webauthn_credential = Fabricate.build(:webauthn_credential, user_id: user.id, nickname: 'USB Key')
new_webauthn_credential.valid?
expect(new_webauthn_credential).to model_have_error_on_field(:nickname)
end
it 'is invalid if sign_count is not a number' do
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 'invalid sign_count')
webauthn_credential.valid?
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
end
it 'is invalid if sign_count is negative number' do
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: -1)
webauthn_credential.valid?
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
end
it 'is invalid if sign_count is greater than the limit' do
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: (described_class::SIGN_COUNT_LIMIT * 2))
webauthn_credential.valid?
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
end
end end
end end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'ActivityPub Likes' do
let(:account) { Fabricate(:account) }
let(:status) { Fabricate :status, account: account }
before { Fabricate :favourite, status: status }
describe 'GET /accounts/:account_username/statuses/:status_id/likes' do
it 'returns http success and activity json types and correct items count' do
get account_status_likes_path(account, status)
expect(response)
.to have_http_status(200)
expect(response.media_type)
.to eq 'application/activity+json'
expect(response.parsed_body)
.to include(type: 'Collection')
.and include(totalItems: 1)
end
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'ActivityPub Shares' do
let(:account) { Fabricate(:account) }
let(:status) { Fabricate :status, account: account }
before { Fabricate :status, reblog: status }
describe 'GET /accounts/:account_username/statuses/:status_id/shares' do
it 'returns http success and activity json types and correct items count' do
get account_status_shares_path(account, status)
expect(response)
.to have_http_status(200)
expect(response.media_type)
.to eq 'application/activity+json'
expect(response.parsed_body)
.to include(type: 'Collection')
.and include(totalItems: 1)
end
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Domain Blocks Previews API' do
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:scopes) { 'write:blocks' }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
let(:account) { Fabricate(:account, user: user) }
describe 'GET /api/v1/domain_blocks/preview' do
subject { get '/api/v1/domain_blocks/preview', params: { domain: domain }, headers: headers }
let(:domain) { 'host.example' }
before do
Fabricate :follow, account: account, target_account: Fabricate(:account, domain: domain)
Fabricate :follow, target_account: account, account: Fabricate(:account, domain: domain)
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it 'returns http success and follower counts' do
subject
expect(response)
.to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body)
.to include(followers_count: 1)
.and include(following_count: 1)
end
end
end

View file

@ -0,0 +1,55 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'API Web Push Subscriptions' do
describe 'DELETE /api/web/push_subscriptions/:id' do
subject { delete api_web_push_subscription_path(token) }
context 'when the subscription exists' do
let!(:web_push_subscription) do
Fabricate(:web_push_subscription)
end
let(:token) do
web_push_subscription.generate_token_for(:unsubscribe)
end
it 'deletes the subscription' do
expect { subject }
.to change(Web::PushSubscription, :count).by(-1)
expect(response).to have_http_status(200)
end
end
context 'when the subscription does not exist' do
let(:web_push_subscription) do
Fabricate(:web_push_subscription)
end
let(:token) do
web_push_subscription.generate_token_for(:unsubscribe)
end
before do
token # memoize before destroying the record
web_push_subscription.destroy!
end
it 'does nothing' do
subject
expect(response).to have_http_status(200)
end
end
context 'when the token is invalid' do
let(:token) { 'invalid--invalid' }
it 'does nothing' do
subject
expect(response).to have_http_status(200)
end
end
end
end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module ProfileStories module ProfileStories
attr_reader :bob, :alice, :alice_bio attr_reader :bob
def fill_in_auth_details(email, password) def fill_in_auth_details(email, password)
fill_in 'user_email', with: email fill_in 'user_email', with: email
@ -31,18 +31,6 @@ module ProfileStories
bob.update!(role: UserRole.find_by!(name: 'Admin')) bob.update!(role: UserRole.find_by!(name: 'Admin'))
end end
def with_alice_as_local_user
@alice_bio = '@alice and @bob are fictional characters commonly used as' \
'placeholder names in #cryptology, as well as #science and' \
'engineering 📖 literature. Not affiliated with @pepe.'
@alice = Fabricate(
:user,
email: 'alice@example.com', password: password, confirmed_at: confirmed_at,
account: Fabricate(:account, username: 'alice', note: @alice_bio)
)
end
def confirmed_at def confirmed_at
@confirmed_at ||= Time.zone.now @confirmed_at ||= Time.zone.now
end end

View file

@ -11,10 +11,10 @@ RSpec.describe 'Profile' do
before do before do
as_a_logged_in_user as_a_logged_in_user
with_alice_as_local_user Fabricate(:user, account: Fabricate(:account, username: 'alice'))
end end
it 'I can view Annes public account' do it 'I can view public account page for Alice' do
visit account_path('alice') visit account_path('alice')
expect(subject).to have_title("alice (@alice@#{local_domain})") expect(subject).to have_title("alice (@alice@#{local_domain})")

View file

@ -7,9 +7,7 @@ RSpec.describe AccountRefreshWorker do
let(:service) { instance_double(ResolveAccountService, call: true) } let(:service) { instance_double(ResolveAccountService, call: true) }
describe '#perform' do describe '#perform' do
before do before { stub_service }
allow(ResolveAccountService).to receive(:new).and_return(service)
end
context 'when account does not exist' do context 'when account does not exist' do
it 'returns immediately without processing' do it 'returns immediately without processing' do
@ -48,5 +46,11 @@ RSpec.describe AccountRefreshWorker do
(Account::BACKGROUND_REFRESH_INTERVAL + 3.days).ago (Account::BACKGROUND_REFRESH_INTERVAL + 3.days).ago
end end
end end
def stub_service
allow(ResolveAccountService)
.to receive(:new)
.and_return(service)
end
end end
end end

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ActivityPub::FollowersSynchronizationWorker do
let(:worker) { described_class.new }
let(:service) { instance_double(ActivityPub::SynchronizeFollowersService, call: true) }
describe '#perform' do
before { stub_service }
let(:account) { Fabricate(:account, domain: 'host.example') }
let(:url) { 'https://sync.url' }
it 'sends the status to the service' do
worker.perform(account.id, url)
expect(service).to have_received(:call).with(account, url)
end
it 'returns nil for non-existent record' do
result = worker.perform(123_123_123, url)
expect(result).to be(true)
end
end
def stub_service
allow(ActivityPub::SynchronizeFollowersService)
.to receive(:new)
.and_return(service)
end
end

View file

@ -6,8 +6,30 @@ RSpec.describe PushConversationWorker do
let(:worker) { described_class.new } let(:worker) { described_class.new }
describe 'perform' do describe 'perform' do
it 'runs without error for missing record' do context 'with missing values' do
expect { worker.perform(nil) }.to_not raise_error it 'runs without error' do
expect { worker.perform(nil) }
.to_not raise_error
end
end
context 'with valid records' do
let(:account_conversation) { Fabricate :account_conversation }
before { allow(redis).to receive(:publish) }
it 'pushes message to timeline' do
expect { worker.perform(account_conversation.id) }
.to_not raise_error
expect(redis)
.to have_received(:publish)
.with(redis_key, anything)
end
def redis_key
"timeline:direct:#{account_conversation.account_id}"
end
end end
end end
end end

View file

@ -6,11 +6,31 @@ RSpec.describe PushUpdateWorker do
let(:worker) { described_class.new } let(:worker) { described_class.new }
describe 'perform' do describe 'perform' do
it 'runs without error for missing record' do context 'with missing values' do
account_id = nil it 'runs without error' do
status_id = nil expect { worker.perform(nil, nil) }
.to_not raise_error
end
end
expect { worker.perform(account_id, status_id) }.to_not raise_error context 'with valid records' do
let(:account) { Fabricate :account }
let(:status) { Fabricate :status }
before { allow(redis).to receive(:publish) }
it 'pushes message to timeline' do
expect { worker.perform(account.id, status.id) }
.to_not raise_error
expect(redis)
.to have_received(:publish)
.with(redis_key, anything)
end
def redis_key
"timeline:#{account.id}"
end
end end
end end
end end

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe RemoteAccountRefreshWorker do
let(:worker) { described_class.new }
let(:service) { instance_double(ActivityPub::FetchRemoteAccountService, call: true) }
describe '#perform' do
before { stub_service }
let(:account) { Fabricate(:account, domain: 'host.example') }
it 'sends the status to the service' do
worker.perform(account.id)
expect(service).to have_received(:call).with(account.uri)
end
it 'returns nil for non-existent record' do
result = worker.perform(123_123_123)
expect(result).to be_nil
end
it 'returns nil for a local record' do
account = Fabricate :account, domain: nil
result = worker.perform(account)
expect(result).to be_nil
end
def stub_service
allow(ActivityPub::FetchRemoteAccountService)
.to receive(:new)
.and_return(service)
end
end
end

View file

@ -4,12 +4,35 @@ require 'rails_helper'
RSpec.describe RemoveFeaturedTagWorker do RSpec.describe RemoveFeaturedTagWorker do
let(:worker) { described_class.new } let(:worker) { described_class.new }
let(:service) { instance_double(RemoveFeaturedTagService, call: true) }
describe 'perform' do describe 'perform' do
it 'runs without error for missing record' do context 'with missing values' do
account_id = nil it 'runs without error' do
featured_tag_id = nil expect { worker.perform(nil, nil) }
expect { worker.perform(account_id, featured_tag_id) }.to_not raise_error .to_not raise_error
end
end
context 'with real records' do
before { stub_service }
let(:account) { Fabricate :account }
let(:featured_tag) { Fabricate :featured_tag }
it 'calls the service for processing' do
worker.perform(account.id, featured_tag.id)
expect(service)
.to have_received(:call)
.with(be_an(Account), be_an(FeaturedTag))
end
def stub_service
allow(RemoveFeaturedTagService)
.to receive(:new)
.and_return(service)
end
end end
end end
end end

View file

@ -4,10 +4,34 @@ require 'rails_helper'
RSpec.describe ResolveAccountWorker do RSpec.describe ResolveAccountWorker do
let(:worker) { described_class.new } let(:worker) { described_class.new }
let(:service) { instance_double(ResolveAccountService, call: true) }
describe 'perform' do describe 'perform' do
it 'runs without error for missing record' do context 'with missing values' do
expect { worker.perform(nil) }.to_not raise_error it 'runs without error' do
expect { worker.perform(nil) }
.to_not raise_error
end
end
context 'with a URI' do
before { stub_service }
let(:uri) { 'https://host/path/value' }
it 'initiates account resolution' do
worker.perform(uri)
expect(service)
.to have_received(:call)
.with(uri)
end
def stub_service
allow(ResolveAccountService)
.to receive(:new)
.and_return(service)
end
end end
end end
end end

View file

@ -61,6 +61,7 @@ RSpec.describe Web::PushNotificationWorker do
'Ttl' => '172800', 'Ttl' => '172800',
'Urgency' => 'normal', 'Urgency' => 'normal',
'Authorization' => 'WebPush jwt.encoded.payload', 'Authorization' => 'WebPush jwt.encoded.payload',
'Unsubscribe-URL' => %r{/api/web/push_subscriptions/},
}, },
body: "+\xB8\xDBT}\u0013\xB6\xDD.\xF9\xB0\xA7\xC8Ҁ\xFD\x99#\xF7\xAC\x83\xA4\xDB,\u001F\xB5\xB9w\x85>\xF7\xADr" body: "+\xB8\xDBT}\u0013\xB6\xDD.\xF9\xB0\xA7\xC8Ҁ\xFD\x99#\xF7\xAC\x83\xA4\xDB,\u001F\xB5\xB9w\x85>\xF7\xADr"
) )

View file

@ -1,7 +1,7 @@
{ {
"name": "@mastodon/streaming", "name": "@mastodon/streaming",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"packageManager": "yarn@4.5.0", "packageManager": "yarn@4.5.1",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },

2587
yarn.lock

File diff suppressed because it is too large Load diff