Merge pull request #2885 from ClearlyClaire/glitch-soc/backports-4.3

Merge upstream changes (stable-4.3)
This commit is contained in:
Claire 2024-10-16 19:56:45 +02:00 committed by GitHub
commit 3a5e83b91a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
79 changed files with 861 additions and 543 deletions

View File

@ -73,6 +73,15 @@ DB_PORT=5432
SECRET_KEY_BASE=
OTP_SECRET=
# Encryption secrets
# ------------------
# Must be available (and set to same values) for all server processes
# These are private/secret values, do not share outside hosting environment
# Use `bin/rails db:encryption:init` to generate fresh secrets
# ------------------
# ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=
# ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=
# ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=
# Web Push
# --------

View File

@ -32,6 +32,8 @@ jobs:
postgres:
- 14-alpine
- 15-alpine
- 16-alpine
- 17-alpine
services:
postgres:

View File

@ -143,7 +143,7 @@ jobs:
uses: ./.github/actions/setup-ruby
with:
ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg libpam-dev
additional-system-dependencies: ffmpeg imagemagick libpam-dev
- name: Load database schema
run: |
@ -245,7 +245,7 @@ jobs:
uses: ./.github/actions/setup-ruby
with:
ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg libpam-dev libyaml-dev
additional-system-dependencies: ffmpeg libpam-dev
- name: Load database schema
run: './bin/rails db:create db:schema:load db:seed'
@ -325,7 +325,7 @@ jobs:
uses: ./.github/actions/setup-ruby
with:
ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg
additional-system-dependencies: ffmpeg imagemagick
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
@ -445,7 +445,7 @@ jobs:
uses: ./.github/actions/setup-ruby
with:
ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg
additional-system-dependencies: ffmpeg imagemagick
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript

View File

@ -23,6 +23,6 @@ class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseControl
private
def set_translation
@translation = TranslateStatusService.new.call(@status, content_locale)
@translation = TranslateStatusService.new.call(@status, I18n.locale.to_s)
end
end

View File

@ -129,8 +129,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
return;
}
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
setCanScrollRight(bodyRef.current.scrollLeft < 0);
} else {
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
}
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
const handleLeftNav = useCallback(() => {
@ -146,8 +151,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
return;
}
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
setCanScrollRight(bodyRef.current.scrollLeft < 0);
} else {
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
}
}, [setCanScrollRight, setCanScrollLeft, bodyRef]);
const handleDismiss = useCallback(() => {

View File

@ -14,6 +14,8 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import { replyCompose } from 'flavours/glitch/actions/compose';
import { toggleReblog, toggleFavourite } from 'flavours/glitch/actions/interactions';
import { openModal } from 'flavours/glitch/actions/modal';
@ -161,16 +163,20 @@ class Footer extends ImmutablePureComponent {
replyTitle = intl.formatMessage(messages.replyAll);
}
let reblogTitle = '';
let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
}
let replyButton = null;
@ -201,7 +207,7 @@ class Footer extends ImmutablePureComponent {
return (
<div className='picture-in-picture__footer'>
{replyButton}
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={StarIcon} onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' iconComponent={OpenInNewIcon} onClick={this.handleOpenClick} href={status.get('url')} />}
</div>

View File

@ -116,6 +116,7 @@ export const MuteModal = ({ accountId, acct }) => {
<div className='safety-action-modal__bottom__collapsible'>
<div className='safety-action-modal__field-group'>
<RadioButtonLabel name='duration' value='0' label={intl.formatMessage(messages.indefinite)} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='21600' label={intl.formatMessage(messages.hours, { number: 6 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='86400' label={intl.formatMessage(messages.hours, { number: 24 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='604800' label={intl.formatMessage(messages.days, { number: 7 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='2592000' label={intl.formatMessage(messages.days, { number: 30 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />

View File

@ -11354,21 +11354,17 @@ noscript {
color: $darker-text-color;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
max-height: 4 * 22px;
max-height: none;
overflow: hidden;
p {
display: none;
&:first-child {
display: initial;
}
}
p,
a {
color: inherit;
}
p {
margin-bottom: 8px;
}
}
.reply-indicator__attachments {

View File

@ -90,6 +90,10 @@ body.rtl {
direction: rtl;
}
.column-back-button__icon {
transform: scale(-1, 1);
}
.simple_form select {
background: $ui-base-color
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 12%))}'/></svg>")

View File

@ -129,8 +129,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
return;
}
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
setCanScrollRight(bodyRef.current.scrollLeft < 0);
} else {
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
}
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
const handleLeftNav = useCallback(() => {
@ -146,8 +151,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
return;
}
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
setCanScrollRight(bodyRef.current.scrollLeft < 0);
} else {
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
}
}, [setCanScrollRight, setCanScrollLeft, bodyRef]);
const handleDismiss = useCallback(() => {

View File

@ -14,6 +14,8 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import { replyCompose } from 'mastodon/actions/compose';
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
import { openModal } from 'mastodon/actions/modal';
@ -159,22 +161,26 @@ class Footer extends ImmutablePureComponent {
replyTitle = intl.formatMessage(messages.replyAll);
}
let reblogTitle = '';
let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
}
return (
<div className='picture-in-picture__footer'>
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={StarIcon} onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' iconComponent={OpenInNewIcon} onClick={this.handleOpenClick} href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} />}
</div>

View File

@ -116,6 +116,7 @@ export const MuteModal = ({ accountId, acct }) => {
<div className='safety-action-modal__bottom__collapsible'>
<div className='safety-action-modal__field-group'>
<RadioButtonLabel name='duration' value='0' label={intl.formatMessage(messages.indefinite)} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='21600' label={intl.formatMessage(messages.hours, { number: 6 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='86400' label={intl.formatMessage(messages.hours, { number: 24 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='604800' label={intl.formatMessage(messages.days, { number: 7 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='2592000' label={intl.formatMessage(messages.days, { number: 30 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />

View File

@ -10804,21 +10804,17 @@ noscript {
color: $darker-text-color;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
max-height: 4 * 22px;
max-height: none;
overflow: hidden;
p {
display: none;
&:first-child {
display: initial;
}
}
p,
a {
color: inherit;
}
p {
margin-bottom: 8px;
}
}
.reply-indicator__attachments {

View File

@ -35,6 +35,10 @@ body.rtl {
direction: rtl;
}
.column-back-button__icon {
transform: scale(-1, 1);
}
.simple_form select {
background: $ui-base-color
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 12%))}'/></svg>")

View File

@ -40,7 +40,7 @@ class ContentSecurityPolicy
end
def cdn_host_value
s3_alias_host || s3_cloudfront_host || azure_alias_host || s3_hostname_host
s3_alias_host || s3_cloudfront_host || azure_alias_host || s3_hostname_host || swift_object_url
end
def paperclip_root_url
@ -76,6 +76,14 @@ class ContentSecurityPolicy
host_to_url ENV.fetch('S3_HOSTNAME', nil)
end
def swift_object_url
url = ENV.fetch('SWIFT_OBJECT_URL', nil)
return if url.blank? || !url.start_with?('https://')
url += '/' unless url.end_with?('/')
url
end
def uri_from_configuration_and_string(host_string)
Addressable::URI.parse("#{host_protocol}://#{host_string}").tap do |uri|
uri.path += '/' unless uri.path.blank? || uri.path.end_with?('/')

View File

@ -18,5 +18,6 @@ class FollowRecommendation < ApplicationRecord
belongs_to :account_summary, foreign_key: :account_id, inverse_of: false
belongs_to :account
scope :localized, ->(locale) { joins(:account_summary).merge(AccountSummary.localized(locale)) }
scope :unsupressed, -> { where.not(FollowRecommendationSuppression.where(FollowRecommendationSuppression.arel_table[:account_id].eq(arel_table[:account_id])).select(1).arel.exists) }
scope :localized, ->(locale) { unsupressed.joins(:account_summary).merge(AccountSummary.localized(locale)) }
end

View File

@ -9,6 +9,8 @@ class TranslateStatusService < BaseService
def call(status, target_language)
@status = status
@source_texts = source_texts
target_language = target_language.split(/[_-]/).first unless target_languages.include?(target_language)
@target_language = target_language
raise Mastodon::NotPermittedError unless permitted?
@ -32,11 +34,15 @@ class TranslateStatusService < BaseService
def permitted?
return false unless @status.distributable? && TranslationService.configured?
languages[@status.language]&.include?(@target_language)
target_languages.include?(@target_language)
end
def languages
Rails.cache.fetch('translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { TranslationService.configured.languages }
Rails.cache.fetch('translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { translation_backend.languages }
end
def target_languages
languages[@status.language] || []
end
def content_hash

View File

@ -1,4 +1,4 @@
%tr
%tr{ id: dom_id(invite) }
%td
.input-copy
.input-copy__wrapper

View File

@ -20,6 +20,7 @@
- ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
Run `bin/rails db:encryption:init` to generate new secrets and then assign the environment variables.
Do not change the secrets once they are set, as doing so may cause data loss and other issues that will be difficult or impossible to recover from.
MESSAGE
end

View File

@ -7,6 +7,17 @@ namespace :db do
namespace :encryption do
desc 'Generate a set of keys for configuring Active Record encryption in a given environment'
task :init do # rubocop:disable Rails/RakeEnvironment
if %w(
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
).any? { |key| ENV.key?(key) }
pastel = Pastel.new
puts pastel.red(<<~MSG)
WARNING: It looks like encryption secrets have already been set. Please ensure you are not changing secrets for a Mastodon installation that already uses them, as this will cause data loss and other issues that are difficult to recover from.
MSG
end
puts <<~MSG
Add the following secret environment variables to your Mastodon environment (e.g. .env.production), ensure they are shared across all your nodes and do not change them after they are set:#{' '}

View File

@ -1,59 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::InvitesController do
render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end
describe 'GET #index' do
subject { get :index, params: { available: true } }
let!(:invite) { Fabricate(:invite) }
it 'renders index page' do
expect(subject).to render_template :index
expect(response.body)
.to include(invite.code)
end
end
describe 'POST #create' do
subject { post :create, params: { invite: { max_uses: '10', expires_in: 1800 } } }
it 'succeeds to create a invite' do
expect { subject }.to change(Invite, :count).by(1)
expect(subject).to redirect_to admin_invites_path
expect(Invite.last).to have_attributes(user_id: user.id, max_uses: 10)
end
end
describe 'DELETE #destroy' do
subject { delete :destroy, params: { id: invite.id } }
let!(:invite) { Fabricate(:invite, expires_at: nil) }
it 'expires invite' do
expect(subject).to redirect_to admin_invites_path
expect(invite.reload).to be_expired
end
end
describe 'POST #deactivate_all' do
before { Fabricate(:invite, expires_at: nil) }
it 'expires all invites, then redirects to admin_invites_path' do
expect { post :deactivate_all }
.to change { Invite.exists?(expires_at: nil) }
.from(true)
.to(false)
expect(response).to redirect_to admin_invites_path
end
end
end

View File

@ -1,82 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::TagsController do
render_views
before do
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin'))
end
describe 'GET #index' do
before do
Fabricate(:tag)
tag_filter = instance_double(Admin::TagFilter, results: Tag.all)
allow(Admin::TagFilter).to receive(:new).and_return(tag_filter)
end
let(:params) { { order: 'newest' } }
it 'returns http success' do
get :index
expect(response).to have_http_status(200)
expect(response).to render_template(:index)
expect(Admin::TagFilter)
.to have_received(:new)
.with(hash_including(params))
end
describe 'with filters' do
let(:params) { { order: 'newest', name: 'test' } }
it 'returns http success' do
get :index, params: { name: 'test' }
expect(response).to have_http_status(200)
expect(response).to render_template(:index)
expect(Admin::TagFilter)
.to have_received(:new)
.with(hash_including(params))
end
end
end
describe 'GET #show' do
let!(:tag) { Fabricate(:tag) }
before do
get :show, params: { id: tag.id }
end
it 'returns status 200' do
expect(response).to have_http_status(200)
end
end
describe 'PUT #update' do
let!(:tag) { Fabricate(:tag, listable: false) }
context 'with valid params' do
it 'updates the tag' do
put :update, params: { id: tag.id, tag: { listable: '1' } }
expect(response).to redirect_to(admin_tag_path(tag.id))
expect(tag.reload).to be_listable
end
end
context 'with invalid params' do
it 'does not update the tag' do
put :update, params: { id: tag.id, tag: { name: 'cant-change-name' } }
expect(response).to have_http_status(200)
expect(response).to render_template(:show)
end
end
end
end

View File

@ -736,76 +736,4 @@ RSpec.describe StatusesController do
end
end
end
describe 'GET #embed' do
let(:account) { Fabricate(:account) }
let(:status) { Fabricate(:status, account: account) }
context 'when account is suspended' do
let(:account) { Fabricate(:account, suspended: true) }
before do
get :embed, params: { account_username: account.username, id: status.id }
end
it 'returns http gone' do
expect(response).to have_http_status(410)
end
end
context 'when status is a reblog' do
let(:original_account) { Fabricate(:account, domain: 'example.com') }
let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') }
let(:status) { Fabricate(:status, account: account, reblog: original_status) }
before do
get :embed, params: { account_username: status.account.username, id: status.id }
end
it 'returns http not found' do
expect(response).to have_http_status(404)
end
end
context 'when status is public' do
before do
get :embed, params: { account_username: status.account.username, id: status.id }
end
it 'renders status successfully', :aggregate_failures do
expect(response)
.to have_http_status(200)
.and render_template(:embed)
expect(response.headers).to include(
'Vary' => 'Accept, Accept-Language, Cookie',
'Cache-Control' => include('public'),
'Link' => include('activity+json')
)
end
end
context 'when status is private' do
let(:status) { Fabricate(:status, account: account, visibility: :private) }
before do
get :embed, params: { account_username: status.account.username, id: status.id }
end
it 'returns http not found' do
expect(response).to have_http_status(404)
end
end
context 'when status is direct' do
let(:status) { Fabricate(:status, account: account, visibility: :direct) }
before do
get :embed, params: { account_username: status.account.username, id: status.id }
end
it 'returns http not found' do
expect(response).to have_http_status(404)
end
end
end
end

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
Fabricator(:ip_block) do
severity { :sign_up_requires_approval }
ip { sequence(:ip) { |n| "10.0.0.#{n}" } }
end

View File

@ -38,16 +38,23 @@ RSpec.describe AccountReachFinder do
end
describe '#inboxes' do
it 'includes the preferred inbox URL of followers' do
expect(described_class.new(account).inboxes).to include(*[ap_follower_example_com, ap_follower_example_org, ap_follower_with_shared].map(&:preferred_inbox_url))
subject { described_class.new(account).inboxes }
it 'includes the preferred inbox URL of followers and recently mentioned accounts but not unrelated users' do
expect(subject)
.to include(*follower_inbox_urls)
.and include(*mentioned_account_inbox_urls)
.and not_include(unrelated_account.preferred_inbox_url)
end
it 'includes the preferred inbox URL of recently-mentioned accounts' do
expect(described_class.new(account).inboxes).to include(*[ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org].map(&:preferred_inbox_url))
def follower_inbox_urls
[ap_follower_example_com, ap_follower_example_org, ap_follower_with_shared]
.map(&:preferred_inbox_url)
end
it 'does not include the inbox of unrelated users' do
expect(described_class.new(account).inboxes).to_not include(unrelated_account.preferred_inbox_url)
def mentioned_account_inbox_urls
[ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org]
.map(&:preferred_inbox_url)
end
end
end

View File

@ -3,6 +3,8 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Block do
subject { described_class.new(json, sender) }
let(:sender) { Fabricate(:account) }
let(:recipient) { Fabricate(:account) }
@ -16,93 +18,65 @@ RSpec.describe ActivityPub::Activity::Block do
}.with_indifferent_access
end
context 'when the recipient does not follow the sender' do
describe '#perform' do
subject { described_class.new(json, sender) }
before do
subject.perform
end
describe '#perform' do
context 'when the recipient does not follow the sender' do
it 'creates a block from sender to recipient' do
expect(sender.blocking?(recipient)).to be true
subject.perform
expect(sender)
.to be_blocking(recipient)
end
end
end
context 'when the recipient is already blocked' do
before do
sender.block!(recipient, uri: 'old')
context 'when the recipient is already blocked' do
before { sender.block!(recipient, uri: 'old') }
it 'creates a block from sender to recipient and sets uri to last received block activity' do
subject.perform
expect(sender)
.to be_blocking(recipient)
expect(sender.block_relationships.find_by(target_account: recipient).uri)
.to eq 'foo'
end
end
describe '#perform' do
subject { described_class.new(json, sender) }
context 'when the recipient follows the sender' do
before { recipient.follow!(sender) }
it 'creates a block from sender to recipient and ensures recipient not following sender' do
subject.perform
expect(sender)
.to be_blocking(recipient)
expect(recipient)
.to_not be_following(sender)
end
end
context 'when a matching undo has been received first' do
let(:undo_json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'bar',
type: 'Undo',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: json,
}.with_indifferent_access
end
before do
recipient.follow!(sender)
ActivityPub::Activity::Undo.new(undo_json, sender).perform
end
it 'does not create a block from sender to recipient and ensures recipient not following sender' do
subject.perform
end
it 'creates a block from sender to recipient' do
expect(sender.blocking?(recipient)).to be true
end
it 'sets the uri to that of last received block activity' do
expect(sender.block_relationships.find_by(target_account: recipient).uri).to eq 'foo'
end
end
end
context 'when the recipient follows the sender' do
before do
recipient.follow!(sender)
end
describe '#perform' do
subject { described_class.new(json, sender) }
before do
subject.perform
end
it 'creates a block from sender to recipient' do
expect(sender.blocking?(recipient)).to be true
end
it 'ensures recipient is not following sender' do
expect(recipient.following?(sender)).to be false
end
end
end
context 'when a matching undo has been received first' do
let(:undo_json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'bar',
type: 'Undo',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: json,
}.with_indifferent_access
end
before do
recipient.follow!(sender)
ActivityPub::Activity::Undo.new(undo_json, sender).perform
end
describe '#perform' do
subject { described_class.new(json, sender) }
before do
subject.perform
end
it 'does not create a block from sender to recipient' do
expect(sender.blocking?(recipient)).to be false
end
it 'ensures recipient is not following sender' do
expect(recipient.following?(sender)).to be false
expect(sender)
.to_not be_blocking(recipient)
expect(recipient)
.to_not be_following(sender)
end
end
end

View File

@ -14,32 +14,24 @@ RSpec.describe Vacuum::AccessTokensVacuum do
let!(:expired_access_grant) { Fabricate(:access_grant, expires_in: 59.minutes.to_i, created_at: 1.hour.ago) }
let!(:active_access_grant) { Fabricate(:access_grant) }
before do
it 'deletes revoked/expired access tokens and revoked/expired grants, but preserves active tokens/grants' do
subject.perform
end
it 'deletes revoked access tokens' do
expect { revoked_access_token.reload }.to raise_error ActiveRecord::RecordNotFound
end
expect { revoked_access_token.reload }
.to raise_error ActiveRecord::RecordNotFound
expect { expired_access_token.reload }
.to raise_error ActiveRecord::RecordNotFound
it 'deletes expired access tokens' do
expect { expired_access_token.reload }.to raise_error ActiveRecord::RecordNotFound
end
expect { revoked_access_grant.reload }
.to raise_error ActiveRecord::RecordNotFound
expect { expired_access_grant.reload }
.to raise_error ActiveRecord::RecordNotFound
it 'deletes revoked access grants' do
expect { revoked_access_grant.reload }.to raise_error ActiveRecord::RecordNotFound
end
expect { active_access_token.reload }
.to_not raise_error
it 'deletes expired access grants' do
expect { expired_access_grant.reload }.to raise_error ActiveRecord::RecordNotFound
end
it 'does not delete active access tokens' do
expect { active_access_token.reload }.to_not raise_error
end
it 'does not delete active access grants' do
expect { active_access_grant.reload }.to_not raise_error
expect { active_access_grant.reload }
.to_not raise_error
end
end
end

View File

@ -11,16 +11,13 @@ RSpec.describe Vacuum::BackupsVacuum do
let!(:expired_backup) { Fabricate(:backup, created_at: (retention_period + 1.day).ago) }
let!(:current_backup) { Fabricate(:backup) }
before do
it 'deletes backups past the retention period but preserves those within the period' do
subject.perform
end
it 'deletes backups past the retention period' do
expect { expired_backup.reload }.to raise_error ActiveRecord::RecordNotFound
end
it 'does not delete backups within the retention period' do
expect { current_backup.reload }.to_not raise_error
expect { expired_backup.reload }
.to raise_error ActiveRecord::RecordNotFound
expect { current_backup.reload }
.to_not raise_error
end
end
end

View File

@ -14,11 +14,11 @@ RSpec.describe Vacuum::FeedsVacuum do
redis.zadd(feed_key_for(active_user), 1, 1)
redis.zadd(feed_key_for(inactive_user, 'reblogs'), 2, 2)
redis.sadd(feed_key_for(inactive_user, 'reblogs:2'), 3)
subject.perform
end
it 'clears feeds of inactive users and lists' do
subject.perform
expect(redis.zcard(feed_key_for(inactive_user))).to eq 0
expect(redis.zcard(feed_key_for(active_user))).to eq 1
expect(redis.exists?(feed_key_for(inactive_user, 'reblogs'))).to be false

View File

@ -17,9 +17,9 @@ RSpec.describe Vacuum::MediaAttachmentsVacuum do
let!(:old_unattached_media) { Fabricate(:media_attachment, account_id: nil, created_at: 10.days.ago) }
let!(:new_unattached_media) { Fabricate(:media_attachment, account_id: nil, created_at: 1.hour.ago) }
before { subject.perform }
it 'handles attachments based on metadata details' do
subject.perform
expect(old_remote_media.reload.file) # Remote and past retention period
.to be_blank
expect(old_local_media.reload.file) # Local and past retention

View File

@ -15,24 +15,22 @@ RSpec.describe Vacuum::PreviewCardsVacuum do
before do
old_preview_card.statuses << Fabricate(:status)
new_preview_card.statuses << Fabricate(:status)
end
it 'handles preview card cleanup' do
subject.perform
end
it 'deletes cache of preview cards last updated before the retention period' do
expect(old_preview_card.reload.image).to be_blank
end
expect(old_preview_card.reload.image) # last updated before retention period
.to be_blank
it 'does not delete cache of preview cards last updated within the retention period' do
expect(new_preview_card.reload.image).to_not be_blank
end
expect(new_preview_card.reload.image) # last updated within the retention period
.to_not be_blank
it 'does not delete attached preview cards' do
expect(new_preview_card.reload).to be_persisted
end
expect(new_preview_card.reload) # Keep attached preview cards
.to be_persisted
it 'does not delete orphaned preview cards in the retention period' do
expect(orphaned_preview_card.reload).to be_persisted
expect(orphaned_preview_card.reload) # keep orphaned cards in the retention period
.to be_persisted
end
end
end

View File

@ -15,24 +15,20 @@ RSpec.describe Vacuum::StatusesVacuum do
let!(:local_status_old) { Fabricate(:status, created_at: (retention_period + 2.days).ago) }
let!(:local_status_recent) { Fabricate(:status, created_at: (retention_period - 2.days).ago) }
before do
it 'deletes remote statuses past the retention period and keeps others' do
subject.perform
end
it 'deletes remote statuses past the retention period' do
expect { remote_status_old.reload }.to raise_error ActiveRecord::RecordNotFound
end
expect { remote_status_old.reload }
.to raise_error ActiveRecord::RecordNotFound
it 'does not delete local statuses past the retention period' do
expect { local_status_old.reload }.to_not raise_error
end
expect { local_status_old.reload }
.to_not raise_error
it 'does not delete remote statuses within the retention period' do
expect { remote_status_recent.reload }.to_not raise_error
end
expect { remote_status_recent.reload }
.to_not raise_error
it 'does not delete local statuses within the retention period' do
expect { local_status_recent.reload }.to_not raise_error
expect { local_status_recent.reload }
.to_not raise_error
end
end
end

View File

@ -16,6 +16,8 @@ RSpec.describe AccountStatusesCleanupPolicy do
describe 'save hooks' do
context 'when widening a policy' do
subject { account_statuses_cleanup_policy.last_inspected }
let!(:account_statuses_cleanup_policy) do
Fabricate(:account_statuses_cleanup_policy,
account: account,
@ -33,64 +35,64 @@ RSpec.describe AccountStatusesCleanupPolicy do
account_statuses_cleanup_policy.record_last_inspected(42)
end
it 'invalidates last_inspected when widened because of keep_direct' do
account_statuses_cleanup_policy.keep_direct = false
account_statuses_cleanup_policy.save
expect(account_statuses_cleanup_policy.last_inspected).to be_nil
context 'when widened because of keep_direct' do
before { account_statuses_cleanup_policy.update(keep_direct: false) }
it { is_expected.to be_nil }
end
it 'invalidates last_inspected when widened because of keep_pinned' do
account_statuses_cleanup_policy.keep_pinned = false
account_statuses_cleanup_policy.save
expect(account_statuses_cleanup_policy.last_inspected).to be_nil
context 'when widened because of keep_pinned' do
before { account_statuses_cleanup_policy.update(keep_pinned: false) }
it { is_expected.to be_nil }
end
it 'invalidates last_inspected when widened because of keep_polls' do
account_statuses_cleanup_policy.keep_polls = false
account_statuses_cleanup_policy.save
expect(account_statuses_cleanup_policy.last_inspected).to be_nil
context 'when widened because of keep_polls' do
before { account_statuses_cleanup_policy.update(keep_polls: false) }
it { is_expected.to be_nil }
end
it 'invalidates last_inspected when widened because of keep_media' do
account_statuses_cleanup_policy.keep_media = false
account_statuses_cleanup_policy.save
expect(account_statuses_cleanup_policy.last_inspected).to be_nil
context 'when widened because of keep_media' do
before { account_statuses_cleanup_policy.update(keep_media: false) }
it { is_expected.to be_nil }
end
it 'invalidates last_inspected when widened because of keep_self_fav' do
account_statuses_cleanup_policy.keep_self_fav = false
account_statuses_cleanup_policy.save
expect(account_statuses_cleanup_policy.last_inspected).to be_nil
context 'when widened because of keep_self_fav' do
before { account_statuses_cleanup_policy.update(keep_self_fav: false) }
it { is_expected.to be_nil }
end
it 'invalidates last_inspected when widened because of keep_self_bookmark' do
account_statuses_cleanup_policy.keep_self_bookmark = false
account_statuses_cleanup_policy.save
expect(account_statuses_cleanup_policy.last_inspected).to be_nil
context 'when widened because of keep_self_bookmark' do
before { account_statuses_cleanup_policy.update(keep_self_bookmark: false) }
it { is_expected.to be_nil }
end
it 'invalidates last_inspected when widened because of higher min_favs' do
account_statuses_cleanup_policy.min_favs = 5
account_statuses_cleanup_policy.save
expect(account_statuses_cleanup_policy.last_inspected).to be_nil
context 'when widened because of higher min_favs' do
before { account_statuses_cleanup_policy.update(min_favs: 5) }
it { is_expected.to be_nil }
end
it 'invalidates last_inspected when widened because of disabled min_favs' do
account_statuses_cleanup_policy.min_favs = nil
account_statuses_cleanup_policy.save
expect(account_statuses_cleanup_policy.last_inspected).to be_nil
context 'when widened because of disabled min_favs' do
before { account_statuses_cleanup_policy.update(min_favs: nil) }
it { is_expected.to be_nil }
end
it 'invalidates last_inspected when widened because of higher min_reblogs' do
account_statuses_cleanup_policy.min_reblogs = 5
account_statuses_cleanup_policy.save
expect(account_statuses_cleanup_policy.last_inspected).to be_nil
context 'when widened because of higher min_reblogs' do
before { account_statuses_cleanup_policy.update(min_reblogs: 5) }
it { is_expected.to be_nil }
end
it 'invalidates last_inspected when widened because of disable min_reblogs' do
account_statuses_cleanup_policy.min_reblogs = nil
account_statuses_cleanup_policy.save
expect(account_statuses_cleanup_policy.last_inspected).to be_nil
context 'when widened because of disable min_reblogs' do
before { account_statuses_cleanup_policy.update(min_reblogs: nil) }
it { is_expected.to be_nil }
end
end

View File

@ -3,11 +3,37 @@
require 'rails_helper'
RSpec.describe Block do
describe 'validations' do
describe 'Associations' do
it { is_expected.to belong_to(:account).required }
it { is_expected.to belong_to(:target_account).required }
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 :block, 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
end
it 'removes blocking cache after creation' do
account = Fabricate(:account)
target_account = Fabricate(:account)

View File

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe Poll do
describe 'scopes' do
describe 'Scopes' do
let(:status) { Fabricate(:status) }
let(:attached_poll) { Fabricate(:poll, status: status) }
let(:not_attached_poll) do
@ -13,7 +13,7 @@ RSpec.describe Poll do
end
end
describe 'attached' do
describe '.attached' do
it 'finds the correct records' do
results = described_class.attached
@ -21,7 +21,7 @@ RSpec.describe Poll do
end
end
describe 'unattached' do
describe '.unattached' do
it 'finds the correct records' do
results = described_class.unattached
@ -30,11 +30,23 @@ RSpec.describe Poll do
end
end
describe 'validations' do
context 'when not valid' do
subject { Fabricate.build(:poll) }
describe '#reset_votes!' do
let(:poll) { Fabricate :poll, cached_tallies: [2, 3], votes_count: 5, voters_count: 5 }
let!(:vote) { Fabricate :poll_vote, poll: }
it { is_expected.to validate_presence_of(:expires_at) }
it 'resets vote data and deletes votes' do
expect { poll.reset_votes! }
.to change(poll, :cached_tallies).to([0, 0])
.and change(poll, :votes_count).to(0)
.and(change(poll, :voters_count).to(0))
expect { vote.reload }
.to raise_error(ActiveRecord::RecordNotFound)
end
end
describe 'Validations' do
subject { Fabricate.build(:poll) }
it { is_expected.to validate_presence_of(:expires_at) }
end
end

View File

@ -55,12 +55,10 @@ RSpec.describe PublicFeed do
context 'without a viewer' do
let(:viewer) { nil }
it 'includes remote instances statuses' do
expect(subject).to include(remote_status.id)
end
it 'includes local statuses' do
expect(subject).to include(local_status.id)
it 'includes remote instances statuses and local statuses' do
expect(subject)
.to include(remote_status.id)
.and include(local_status.id)
end
it 'does not include local-only statuses' do
@ -71,12 +69,10 @@ RSpec.describe PublicFeed do
context 'with a viewer' do
let(:viewer) { Fabricate(:account, username: 'viewer') }
it 'includes remote instances statuses' do
expect(subject).to include(remote_status.id)
end
it 'includes local statuses' do
expect(subject).to include(local_status.id)
it 'includes remote instances statuses and local statuses' do
expect(subject)
.to include(remote_status.id)
.and include(local_status.id)
end
it 'does not include local-only statuses' do

View File

@ -387,23 +387,43 @@ RSpec.describe User do
end
end
describe 'token_for_app' do
describe '#token_for_app' do
let(:user) { Fabricate(:user) }
let(:app) { Fabricate(:application, owner: user) }
it 'returns a token' do
expect(user.token_for_app(app)).to be_a(Doorkeeper::AccessToken)
context 'when user owns app but does not have tokens' do
let(:app) { Fabricate(:application, owner: user) }
it 'creates and returns a persisted token' do
expect { user.token_for_app(app) }
.to change(Doorkeeper::AccessToken.where(resource_owner_id: user.id, application: app), :count).by(1)
end
end
it 'persists a token' do
t = user.token_for_app(app)
expect(user.token_for_app(app)).to eql(t)
context 'when user owns app and already has tokens' do
let(:app) { Fabricate(:application, owner: user) }
let!(:token) { Fabricate :access_token, application: app, resource_owner_id: user.id }
it 'returns a persisted token' do
expect(user.token_for_app(app))
.to be_a(Doorkeeper::AccessToken)
.and eq(token)
end
end
it 'is nil if user does not own app' do
app.update!(owner: nil)
context 'when user does not own app' do
let(:app) { Fabricate(:application) }
expect(user.token_for_app(app)).to be_nil
it 'returns nil' do
expect(user.token_for_app(app))
.to be_nil
end
end
context 'when app is nil' do
it 'returns nil' do
expect(user.token_for_app(nil))
.to be_nil
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe AccountModerationNotePolicy do
subject { described_class }
@ -12,13 +11,13 @@ RSpec.describe AccountModerationNotePolicy do
permissions :create? do
context 'when staff' do
it 'grants to create' do
expect(subject).to permit(admin, described_class)
expect(subject).to permit(admin, AccountModerationNote)
end
end
context 'when not staff' do
it 'denies to create' do
expect(subject).to_not permit(john, described_class)
expect(subject).to_not permit(john, AccountModerationNote)
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe AccountPolicy do
subject { described_class }
@ -24,7 +23,7 @@ RSpec.describe AccountPolicy do
end
end
permissions :show?, :unsilence?, :unsensitive?, :remove_avatar?, :remove_header? do
permissions :show?, :unsilence?, :unsensitive?, :remove_avatar?, :remove_header?, :sensitive?, :warn? do
context 'when staff' do
it 'permits' do
expect(subject).to permit(admin, alice)

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AccountWarningPolicy do
subject { described_class }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:account) { Fabricate(:account) }
permissions :show? do
context 'with an admin' do
it { is_expected.to permit(admin, AccountWarning.new) }
end
context 'with a non-admin' do
context 'when account is not target' do
it { is_expected.to_not permit(account, AccountWarning.new) }
end
context 'when account is target' do
it { is_expected.to permit(account, AccountWarning.new(target_account_id: account.id)) }
end
end
end
permissions :appeal? do
context 'when account is not target' do
it { is_expected.to_not permit(account, AccountWarning.new) }
end
context 'when account is target' do
context 'when record is appealable' do
it { is_expected.to permit(account, AccountWarning.new(target_account_id: account.id, created_at: Appeal::MAX_STRIKE_AGE.ago + 1.hour)) }
end
context 'when record is not appealable' do
it { is_expected.to_not permit(account, AccountWarning.new(target_account_id: account.id, created_at: Appeal::MAX_STRIKE_AGE.ago - 1.hour)) }
end
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe AccountWarningPresetPolicy do
let(:policy) { described_class }
@ -11,13 +10,13 @@ RSpec.describe AccountWarningPresetPolicy do
permissions :index?, :create?, :update?, :destroy? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Tag)
expect(policy).to permit(admin, AccountWarningPreset)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Tag)
expect(policy).to_not permit(john, AccountWarningPreset)
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe Admin::StatusPolicy do
let(:policy) { described_class }
@ -13,13 +12,13 @@ RSpec.describe Admin::StatusPolicy do
permissions :index?, :update?, :review?, :destroy? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Tag)
expect(policy).to permit(admin, Status)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Tag)
expect(policy).to_not permit(john, Status)
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe AnnouncementPolicy do
let(:policy) { described_class }
@ -11,13 +10,13 @@ RSpec.describe AnnouncementPolicy do
permissions :index?, :create?, :update?, :destroy? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Tag)
expect(policy).to permit(admin, Announcement)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Tag)
expect(policy).to_not permit(john, Announcement)
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe AppealPolicy do
let(:policy) { described_class }
@ -12,18 +11,18 @@ RSpec.describe AppealPolicy do
permissions :index? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Tag)
expect(policy).to permit(admin, Appeal)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Tag)
expect(policy).to_not permit(john, Appeal)
end
end
end
permissions :reject? do
permissions :reject?, :approve? do
context 'with an admin' do
context 'with a pending appeal' do
before { allow(appeal).to receive(:pending?).and_return(true) }

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AuditLogPolicy do
subject { described_class }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:account) { Fabricate(:account) }
permissions :index? do
context 'with an admin' do
it { is_expected.to permit(admin, nil) }
end
context 'with a non-admin' do
it { is_expected.to_not permit(account, nil) }
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe BackupPolicy do
subject { described_class }

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe CanonicalEmailBlockPolicy do
let(:policy) { described_class }
@ -11,13 +10,13 @@ RSpec.describe CanonicalEmailBlockPolicy do
permissions :index?, :show?, :test?, :create?, :destroy? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Tag)
expect(policy).to permit(admin, CanonicalEmailBlock)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Tag)
expect(policy).to_not permit(john, CanonicalEmailBlock)
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe CustomEmojiPolicy do
subject { described_class }

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe DashboardPolicy do
subject { described_class }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:account) { Fabricate(:account) }
permissions :index? do
context 'with an admin' do
it { is_expected.to permit(admin, nil) }
end
context 'with a non-admin' do
it { is_expected.to_not permit(account, nil) }
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe DeliveryPolicy do
let(:policy) { described_class }
@ -11,13 +10,13 @@ RSpec.describe DeliveryPolicy do
permissions :clear_delivery_errors?, :restart_delivery?, :stop_delivery? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Tag)
expect(policy).to permit(admin, nil)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Tag)
expect(policy).to_not permit(john, nil)
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe DomainAllowPolicy do
subject { described_class }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :index?, :show?, :create?, :destroy? do
context 'when admin' do
it 'permits' do
expect(subject).to permit(admin, DomainAllow)
end
end
context 'when not admin' do
it 'denies' do
expect(subject).to_not permit(john, DomainAllow)
end
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe DomainBlockPolicy do
subject { described_class }
@ -9,7 +8,7 @@ RSpec.describe DomainBlockPolicy do
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :index?, :show?, :create?, :destroy? do
permissions :index?, :show?, :create?, :destroy?, :update? do
context 'when admin' do
it 'permits' do
expect(subject).to permit(admin, DomainBlock)

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe EmailDomainBlockPolicy do
subject { described_class }

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe FollowRecommendationPolicy do
let(:policy) { described_class }
@ -11,13 +10,13 @@ RSpec.describe FollowRecommendationPolicy do
permissions :show?, :suppress?, :unsuppress? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Tag)
expect(policy).to permit(admin, FollowRecommendation)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Tag)
expect(policy).to_not permit(john, FollowRecommendation)
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe InstancePolicy do
subject { described_class }

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe InvitePolicy do
subject { described_class }

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe IpBlockPolicy do
let(:policy) { described_class }
@ -11,13 +10,13 @@ RSpec.describe IpBlockPolicy do
permissions :index?, :show?, :create?, :update?, :destroy? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Tag)
expect(policy).to permit(admin, IpBlock)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Tag)
expect(policy).to_not permit(john, IpBlock)
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe PollPolicy do
subject { described_class }
let(:account) { Fabricate(:account) }
let(:poll) { Fabricate :poll }
permissions :vote? do
context 'when account cannot view status' do
before { poll.status.update(visibility: :private) }
it { is_expected.to_not permit(account, poll) }
end
context 'when account can view status' do
context 'when accounts do not block each other' do
it { is_expected.to permit(account, poll) }
end
context 'when view blocks poll creator' do
before { Fabricate :block, account: account, target_account: poll.account }
it { is_expected.to_not permit(account, poll) }
end
context 'when poll creator blocks viewer' do
before { Fabricate :block, account: poll.account, target_account: account }
it { is_expected.to_not permit(account, poll) }
end
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe PreviewCardPolicy do
let(:policy) { described_class }
@ -11,13 +10,13 @@ RSpec.describe PreviewCardPolicy do
permissions :index?, :review? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Tag)
expect(policy).to permit(admin, PreviewCard)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Tag)
expect(policy).to_not permit(john, PreviewCard)
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe PreviewCardProviderPolicy do
let(:policy) { described_class }
@ -11,13 +10,13 @@ RSpec.describe PreviewCardProviderPolicy do
permissions :index?, :review? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Tag)
expect(policy).to permit(admin, PreviewCardProvider)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Tag)
expect(policy).to_not permit(john, PreviewCardProvider)
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe RelayPolicy do
subject { described_class }

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe ReportNotePolicy do
subject { described_class }

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe ReportPolicy do
subject { described_class }

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe RulePolicy do
let(:policy) { described_class }
@ -11,13 +10,13 @@ RSpec.describe RulePolicy do
permissions :index?, :create?, :update?, :destroy? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Tag)
expect(policy).to permit(admin, Rule)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Tag)
expect(policy).to_not permit(john, Rule)
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe SettingsPolicy do
subject { described_class }

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe SoftwareUpdatePolicy do
subject { described_class }

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe StatusPolicy, type: :model do
subject { described_class }

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe TagPolicy do
subject { described_class }

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe UserPolicy do
subject { described_class }
@ -112,4 +111,42 @@ RSpec.describe UserPolicy do
end
end
end
permissions :approve?, :reject? do
context 'when admin' do
context 'when user is approved' do
it { is_expected.to_not permit(admin, User.new(approved: true)) }
end
context 'when user is not approved' do
it { is_expected.to permit(admin, User.new(approved: false)) }
end
end
context 'when not admin' do
it { is_expected.to_not permit(john, User.new) }
end
end
permissions :change_role? do
context 'when not admin' do
it { is_expected.to_not permit(john, User.new) }
end
context 'when admin' do
let(:user) { User.new(role: role) }
context 'when role of admin overrides user role' do
let(:role) { UserRole.new(position: admin.user.role.position - 10, id: 123) }
it { is_expected.to permit(admin, user) }
end
context 'when role of admin does not override user role' do
let(:role) { UserRole.new(position: admin.user.role.position + 10, id: 123) }
it { is_expected.to_not permit(admin, user) }
end
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe UserRolePolicy do
subject { described_class }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:account) { Fabricate(:account) }
permissions :index?, :create? do
context 'when admin' do
it { is_expected.to permit(admin, UserRole.new) }
end
context 'when not admin' do
it { is_expected.to_not permit(account, UserRole.new) }
end
end
permissions :update? do
context 'when admin' do
context 'when role of admin overrides relevant role' do
it { is_expected.to permit(admin, UserRole.new(position: admin.user.role.position - 10, id: 123)) }
end
context 'when role of admin does not override relevant role' do
it { is_expected.to_not permit(admin, UserRole.new(position: admin.user.role.position + 10, id: 123)) }
end
end
context 'when not admin' do
it { is_expected.to_not permit(account, UserRole.new) }
end
end
permissions :destroy? do
context 'when admin' do
context 'when role of admin overrides relevant role' do
it { is_expected.to permit(admin, UserRole.new(position: admin.user.role.position - 10)) }
end
context 'when role of admin does not override relevant role' do
it { is_expected.to_not permit(admin, UserRole.new(position: admin.user.role.position + 10)) }
end
context 'when everyone role' do
it { is_expected.to_not permit(admin, UserRole.everyone) }
end
end
context 'when not admin' do
it { is_expected.to_not permit(account, UserRole.new) }
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rails_helper'
require 'pundit/rspec'
RSpec.describe WebhookPolicy do
let(:policy) { described_class }

View File

@ -43,6 +43,7 @@ require 'paperclip/matchers'
require 'capybara/rspec'
require 'chewy/rspec'
require 'email_spec/rspec'
require 'pundit/rspec'
require 'test_prof/recipes/rspec/before_all'
Rails.root.glob('spec/support/**/*.rb').each { |f| require f }

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Status embed' do
describe 'GET /users/:account_username/statuses/:id/embed' do
subject { get "/users/#{account.username}/statuses/#{status.id}/embed" }
let(:account) { Fabricate(:account) }
let(:status) { Fabricate(:status, account: account) }
context 'when account is suspended' do
let(:account) { Fabricate(:account, suspended: true) }
it 'returns http gone' do
subject
expect(response)
.to have_http_status(410)
end
end
context 'when status is a reblog' do
let(:original_account) { Fabricate(:account, domain: 'example.com') }
let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') }
let(:status) { Fabricate(:status, account: account, reblog: original_status) }
it 'returns http not found' do
subject
expect(response)
.to have_http_status(404)
end
end
context 'when status is public' do
it 'renders status successfully', :aggregate_failures do
subject
expect(response)
.to have_http_status(200)
expect(response.parsed_body.at('body.embed'))
.to be_present
expect(response.headers).to include(
'Vary' => 'Accept, Accept-Language, Cookie',
'Cache-Control' => include('public'),
'Link' => include('activity+json')
)
end
end
context 'when status is private' do
let(:status) { Fabricate(:status, account: account, visibility: :private) }
it 'returns http not found' do
subject
expect(response)
.to have_http_status(404)
end
end
context 'when status is direct' do
let(:status) { Fabricate(:status, account: account, visibility: :direct) }
it 'returns http not found' do
subject
expect(response)
.to have_http_status(404)
end
end
end
end

View File

@ -14,7 +14,7 @@ RSpec.describe ActivityPub::NoteSerializer do
let!(:reply_by_account_third) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) }
it 'has the expected shape' do
it 'has the expected shape and replies collection' do
expect(subject).to include({
'@context' => include('https://www.w3.org/ns/activitystreams'),
'type' => 'Note',
@ -22,26 +22,23 @@ RSpec.describe ActivityPub::NoteSerializer do
'contentMap' => include({
'zh-TW' => a_kind_of(String),
}),
'replies' => replies_collection_values,
})
end
it 'has a replies collection' do
expect(subject['replies']['type']).to eql('Collection')
def replies_collection_values
include(
'type' => eql('Collection'),
'first' => include(
'type' => eql('CollectionPage'),
'items' => reply_items
)
)
end
it 'has a replies collection with a first Page' do
expect(subject['replies']['first']['type']).to eql('CollectionPage')
end
it 'includes public self-replies in its replies collection' do
expect(subject['replies']['first']['items']).to include(reply_by_account_first.uri, reply_by_account_next.uri, reply_by_account_third.uri)
end
it 'does not include replies from others in its replies collection' do
expect(subject['replies']['first']['items']).to_not include(reply_by_other_first.uri)
end
it 'does not include replies with direct visibility in its replies collection' do
expect(subject['replies']['first']['items']).to_not include(reply_by_account_visibility_direct.uri)
def reply_items
include(reply_by_account_first.uri, reply_by_account_next.uri, reply_by_account_third.uri) # Public self replies
.and(not_include(reply_by_other_first.uri)) # Replies from others
.and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility
end
end

View File

@ -18,7 +18,7 @@ RSpec.describe TranslateStatusService do
describe '#call' do
before do
translation_service = TranslationService.new
allow(translation_service).to receive(:languages).and_return({ 'en' => ['es'] })
allow(translation_service).to receive(:languages).and_return({ 'en' => ['es', 'es-MX'] })
allow(translation_service).to receive(:translate) do |texts|
texts.map do |text|
TranslationService::Translation.new(
@ -37,6 +37,7 @@ RSpec.describe TranslateStatusService do
.to have_attributes(
content: '<p>Hola</p>',
detected_source_language: 'en',
language: 'es',
provider: 'Dummy',
status: status
)
@ -101,6 +102,16 @@ RSpec.describe TranslateStatusService do
expect(media_attachment.description).to eq 'Hola & :highfive:'
end
end
describe 'target language is regional' do
it 'uses regional variant' do
expect(service.call(status, 'es-MX').language).to eq 'es-MX'
end
it 'uses parent locale for unsupported regional variant' do
expect(service.call(status, 'es-XX').language).to eq 'es'
end
end
end
describe '#source_texts' do

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Admin Invites' do
describe 'Invite interaction' do
let!(:invite) { Fabricate(:invite, expires_at: nil) }
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before { sign_in user }
it 'allows invite listing and creation' do
visit admin_invites_path
expect(page)
.to have_title(I18n.t('admin.invites.title'))
for_invite(invite) do
expect(find('input').value)
.to include(invite.code)
end
select I18n.t('invites.max_uses', count: 10), from: max_use_field
expect { generate_invite }
.to change(Invite, :count).by(1)
expect(user.invites.last)
.to have_attributes(max_uses: 10)
end
it 'allows invite expiration' do
visit admin_invites_path
for_invite(invite) do
expect { expire_invite }
.to change { invite.reload.expired? }.from(false).to(true)
end
end
it 'allows invite deactivation' do
visit admin_invites_path
expect { click_on I18n.t('admin.invites.deactivate_all') }
.to change { Invite.exists?(expires_at: nil) }.from(true).to(false)
end
def for_invite(invite, &block)
within("#invite_#{invite.id}", &block)
end
def expire_invite
click_on I18n.t('invites.delete')
end
def generate_invite
click_on I18n.t('invites.generate')
end
def max_use_field
I18n.t('simple_form.labels.defaults.max_uses')
end
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Admin Tags' do
describe 'Tag interaction' do
let!(:tag) { Fabricate(:tag, name: 'test') }
before { sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
it 'allows tags listing and editing' do
visit admin_tags_path
expect(page)
.to have_title(I18n.t('admin.tags.title'))
click_on '#test'
fill_in display_name_field, with: 'NewTagName'
expect { click_on submit_button }
.to_not(change { tag.reload.display_name })
expect(page)
.to have_content(match_error_text)
fill_in display_name_field, with: 'TEST'
expect { click_on submit_button }
.to(change { tag.reload.display_name }.to('TEST'))
end
def display_name_field
I18n.t('simple_form.labels.defaults.display_name')
end
def match_error_text
I18n.t('tags.does_not_match_previous_name')
end
end
end

View File

@ -5,9 +5,50 @@ require 'rails_helper'
RSpec.describe Scheduler::IpCleanupScheduler do
let(:worker) { described_class.new }
describe 'perform' do
it 'runs without error' do
expect { worker.perform }.to_not raise_error
describe '#perform' do
context 'with IP-related data past retention times' do
let!(:future_ip_block) { Fabricate :ip_block, expires_at: 1.week.from_now }
let!(:old_ip_block) { Fabricate :ip_block, expires_at: 1.week.ago }
let!(:session_past_retention) { Fabricate :session_activation, ip: '10.0.0.0', updated_at: 18.months.ago }
let!(:inactive_user) { Fabricate :user, current_sign_in_at: 18.months.ago, sign_up_ip: '10.0.0.0' }
let!(:old_login_activity) { Fabricate :login_activity, created_at: 18.months.ago }
let!(:old_token) { Fabricate :access_token, last_used_at: 18.months.ago, last_used_ip: '10.0.0.0' }
before { stub_const 'Scheduler::IpCleanupScheduler::SESSION_RETENTION_PERIOD', 10.years.to_i.seconds }
it 'deletes the expired block' do
expect { worker.perform }
.to_not raise_error
expect { old_ip_block.reload }
.to raise_error(ActiveRecord::RecordNotFound)
expect { old_login_activity.reload }
.to raise_error(ActiveRecord::RecordNotFound)
expect(session_past_retention.reload.ip)
.to be_nil
expect(inactive_user.reload.sign_up_ip)
.to be_nil
expect(old_token.reload.last_used_ip)
.to be_nil
expect(future_ip_block.reload)
.to be_present
end
end
context 'with old session data' do
let!(:new_activation) { Fabricate :session_activation, updated_at: 1.week.ago }
let!(:old_activation) { Fabricate :session_activation, updated_at: 1.month.ago }
before { stub_const 'Scheduler::IpCleanupScheduler::SESSION_RETENTION_PERIOD', 10.days.to_i.seconds }
it 'clears old sessions' do
expect { worker.perform }
.to_not raise_error
expect { old_activation.reload }
.to raise_error(ActiveRecord::RecordNotFound)
expect(new_activation.reload)
.to be_present
end
end
end
end

View File

@ -50,9 +50,9 @@ function getSentinelConfiguration(env, commonOptions) {
return {
db: redisDatabase,
name: env.REDIS_SENTINEL_MASTER,
username: env.REDIS_USERNAME,
username: env.REDIS_USER,
password: env.REDIS_PASSWORD,
sentinelUsername: env.REDIS_SENTINEL_USERNAME ?? env.REDIS_USERNAME,
sentinelUsername: env.REDIS_SENTINEL_USERNAME ?? env.REDIS_USER,
sentinelPassword: env.REDIS_SENTINEL_PASSWORD ?? env.REDIS_PASSWORD,
sentinels,
...commonOptions,
@ -104,7 +104,7 @@ export function configFromEnv(env) {
host: env.REDIS_HOST ?? '127.0.0.1',
port: redisPort,
db: redisDatabase,
username: env.REDIS_USERNAME,
username: env.REDIS_USER,
password: env.REDIS_PASSWORD,
...commonOptions,
};