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= SECRET_KEY_BASE=
OTP_SECRET= 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 # Web Push
# -------- # --------

View file

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

View file

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

View file

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

View file

@ -129,8 +129,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
return; return;
} }
setCanScrollLeft(bodyRef.current.scrollLeft > 0); if (getComputedStyle(bodyRef.current).direction === 'rtl') {
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth); 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]); }, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
const handleLeftNav = useCallback(() => { const handleLeftNav = useCallback(() => {
@ -146,8 +151,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
return; return;
} }
setCanScrollLeft(bodyRef.current.scrollLeft > 0); if (getComputedStyle(bodyRef.current).direction === 'rtl') {
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth); 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]); }, [setCanScrollRight, setCanScrollLeft, bodyRef]);
const handleDismiss = useCallback(() => { 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 ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.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 { replyCompose } from 'flavours/glitch/actions/compose';
import { toggleReblog, toggleFavourite } from 'flavours/glitch/actions/interactions'; import { toggleReblog, toggleFavourite } from 'flavours/glitch/actions/interactions';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
@ -161,16 +163,20 @@ class Footer extends ImmutablePureComponent {
replyTitle = intl.formatMessage(messages.replyAll); replyTitle = intl.formatMessage(messages.replyAll);
} }
let reblogTitle = ''; let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) { if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private); reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) { } else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog); reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) { } else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private); reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else { } else {
reblogTitle = intl.formatMessage(messages.cannot_reblog); reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
} }
let replyButton = null; let replyButton = null;
@ -201,7 +207,7 @@ class Footer extends ImmutablePureComponent {
return ( return (
<div className='picture-in-picture__footer'> <div className='picture-in-picture__footer'>
{replyButton} {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')} /> <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')} />} {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> </div>

View file

@ -116,6 +116,7 @@ export const MuteModal = ({ accountId, acct }) => {
<div className='safety-action-modal__bottom__collapsible'> <div className='safety-action-modal__bottom__collapsible'>
<div className='safety-action-modal__field-group'> <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='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='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='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} /> <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; color: $darker-text-color;
-webkit-line-clamp: 4; -webkit-line-clamp: 4;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
max-height: 4 * 22px; max-height: none;
overflow: hidden; overflow: hidden;
p {
display: none;
&:first-child {
display: initial;
}
}
p, p,
a { a {
color: inherit; color: inherit;
} }
p {
margin-bottom: 8px;
}
} }
.reply-indicator__attachments { .reply-indicator__attachments {

View file

@ -90,6 +90,10 @@ body.rtl {
direction: rtl; direction: rtl;
} }
.column-back-button__icon {
transform: scale(-1, 1);
}
.simple_form select { .simple_form select {
background: $ui-base-color 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>") 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; return;
} }
setCanScrollLeft(bodyRef.current.scrollLeft > 0); if (getComputedStyle(bodyRef.current).direction === 'rtl') {
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth); 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]); }, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
const handleLeftNav = useCallback(() => { const handleLeftNav = useCallback(() => {
@ -146,8 +151,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
return; return;
} }
setCanScrollLeft(bodyRef.current.scrollLeft > 0); if (getComputedStyle(bodyRef.current).direction === 'rtl') {
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth); 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]); }, [setCanScrollRight, setCanScrollLeft, bodyRef]);
const handleDismiss = useCallback(() => { 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 ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.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 { replyCompose } from 'mastodon/actions/compose';
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions'; import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
@ -159,22 +161,26 @@ class Footer extends ImmutablePureComponent {
replyTitle = intl.formatMessage(messages.replyAll); replyTitle = intl.formatMessage(messages.replyAll);
} }
let reblogTitle = ''; let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) { if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private); reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) { } else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog); reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) { } else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private); reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else { } else {
reblogTitle = intl.formatMessage(messages.cannot_reblog); reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
} }
return ( return (
<div className='picture-in-picture__footer'> <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='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')} /> <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')}`} />} {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> </div>

View file

@ -116,6 +116,7 @@ export const MuteModal = ({ accountId, acct }) => {
<div className='safety-action-modal__bottom__collapsible'> <div className='safety-action-modal__bottom__collapsible'>
<div className='safety-action-modal__field-group'> <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='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='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='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} /> <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; color: $darker-text-color;
-webkit-line-clamp: 4; -webkit-line-clamp: 4;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
max-height: 4 * 22px; max-height: none;
overflow: hidden; overflow: hidden;
p {
display: none;
&:first-child {
display: initial;
}
}
p, p,
a { a {
color: inherit; color: inherit;
} }
p {
margin-bottom: 8px;
}
} }
.reply-indicator__attachments { .reply-indicator__attachments {

View file

@ -35,6 +35,10 @@ body.rtl {
direction: rtl; direction: rtl;
} }
.column-back-button__icon {
transform: scale(-1, 1);
}
.simple_form select { .simple_form select {
background: $ui-base-color 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>") 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 end
def cdn_host_value 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 end
def paperclip_root_url def paperclip_root_url
@ -76,6 +76,14 @@ class ContentSecurityPolicy
host_to_url ENV.fetch('S3_HOSTNAME', nil) host_to_url ENV.fetch('S3_HOSTNAME', nil)
end 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) def uri_from_configuration_and_string(host_string)
Addressable::URI.parse("#{host_protocol}://#{host_string}").tap do |uri| Addressable::URI.parse("#{host_protocol}://#{host_string}").tap do |uri|
uri.path += '/' unless uri.path.blank? || uri.path.end_with?('/') 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_summary, foreign_key: :account_id, inverse_of: false
belongs_to :account 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 end

View file

@ -9,6 +9,8 @@ class TranslateStatusService < BaseService
def call(status, target_language) def call(status, target_language)
@status = status @status = status
@source_texts = source_texts @source_texts = source_texts
target_language = target_language.split(/[_-]/).first unless target_languages.include?(target_language)
@target_language = target_language @target_language = target_language
raise Mastodon::NotPermittedError unless permitted? raise Mastodon::NotPermittedError unless permitted?
@ -32,11 +34,15 @@ class TranslateStatusService < BaseService
def permitted? def permitted?
return false unless @status.distributable? && TranslationService.configured? return false unless @status.distributable? && TranslationService.configured?
languages[@status.language]&.include?(@target_language) target_languages.include?(@target_language)
end end
def languages 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 end
def content_hash def content_hash

View file

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

View file

@ -20,6 +20,7 @@
- ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY - ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
Run `bin/rails db:encryption:init` to generate new secrets and then assign the environment variables. 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 MESSAGE
end end

View file

@ -7,6 +7,17 @@ namespace :db do
namespace :encryption do namespace :encryption do
desc 'Generate a set of keys for configuring Active Record encryption in a given environment' desc 'Generate a set of keys for configuring Active Record encryption in a given environment'
task :init do # rubocop:disable Rails/RakeEnvironment 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 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:#{' '} 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 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 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 end
describe '#inboxes' do describe '#inboxes' do
it 'includes the preferred inbox URL of followers' do subject { described_class.new(account).inboxes }
expect(described_class.new(account).inboxes).to include(*[ap_follower_example_com, ap_follower_example_org, ap_follower_with_shared].map(&:preferred_inbox_url))
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 end
it 'includes the preferred inbox URL of recently-mentioned accounts' do def follower_inbox_urls
expect(described_class.new(account).inboxes).to include(*[ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org].map(&:preferred_inbox_url)) [ap_follower_example_com, ap_follower_example_org, ap_follower_with_shared]
.map(&:preferred_inbox_url)
end end
it 'does not include the inbox of unrelated users' do def mentioned_account_inbox_urls
expect(described_class.new(account).inboxes).to_not include(unrelated_account.preferred_inbox_url) [ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org]
.map(&:preferred_inbox_url)
end end
end end
end end

View file

@ -3,6 +3,8 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe ActivityPub::Activity::Block do RSpec.describe ActivityPub::Activity::Block do
subject { described_class.new(json, sender) }
let(:sender) { Fabricate(:account) } let(:sender) { Fabricate(:account) }
let(:recipient) { Fabricate(:account) } let(:recipient) { Fabricate(:account) }
@ -16,93 +18,65 @@ RSpec.describe ActivityPub::Activity::Block do
}.with_indifferent_access }.with_indifferent_access
end end
context 'when the recipient does not follow the sender' do describe '#perform' do
describe '#perform' do context 'when the recipient does not follow the sender' do
subject { described_class.new(json, sender) }
before do
subject.perform
end
it 'creates a block from sender to recipient' 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 end
end
context 'when the recipient is already blocked' do context 'when the recipient is already blocked' do
before do before { sender.block!(recipient, uri: 'old') }
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 end
describe '#perform' do context 'when the recipient follows the sender' do
subject { described_class.new(json, sender) } 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 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 subject.perform
end
it 'creates a block from sender to recipient' do expect(sender)
expect(sender.blocking?(recipient)).to be true .to_not be_blocking(recipient)
end expect(recipient)
.to_not be_following(sender)
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
end end
end 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!(:expired_access_grant) { Fabricate(:access_grant, expires_in: 59.minutes.to_i, created_at: 1.hour.ago) }
let!(:active_access_grant) { Fabricate(:access_grant) } 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 subject.perform
end
it 'deletes revoked access tokens' do expect { revoked_access_token.reload }
expect { revoked_access_token.reload }.to raise_error ActiveRecord::RecordNotFound .to raise_error ActiveRecord::RecordNotFound
end expect { expired_access_token.reload }
.to raise_error ActiveRecord::RecordNotFound
it 'deletes expired access tokens' do expect { revoked_access_grant.reload }
expect { expired_access_token.reload }.to raise_error ActiveRecord::RecordNotFound .to raise_error ActiveRecord::RecordNotFound
end expect { expired_access_grant.reload }
.to raise_error ActiveRecord::RecordNotFound
it 'deletes revoked access grants' do expect { active_access_token.reload }
expect { revoked_access_grant.reload }.to raise_error ActiveRecord::RecordNotFound .to_not raise_error
end
it 'deletes expired access grants' do expect { active_access_grant.reload }
expect { expired_access_grant.reload }.to raise_error ActiveRecord::RecordNotFound .to_not raise_error
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
end end
end 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!(:expired_backup) { Fabricate(:backup, created_at: (retention_period + 1.day).ago) }
let!(:current_backup) { Fabricate(:backup) } let!(:current_backup) { Fabricate(:backup) }
before do it 'deletes backups past the retention period but preserves those within the period' do
subject.perform subject.perform
end
it 'deletes backups past the retention period' do expect { expired_backup.reload }
expect { expired_backup.reload }.to raise_error ActiveRecord::RecordNotFound .to raise_error ActiveRecord::RecordNotFound
end expect { current_backup.reload }
.to_not raise_error
it 'does not delete backups within the retention period' do
expect { current_backup.reload }.to_not raise_error
end end
end 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(active_user), 1, 1)
redis.zadd(feed_key_for(inactive_user, 'reblogs'), 2, 2) redis.zadd(feed_key_for(inactive_user, 'reblogs'), 2, 2)
redis.sadd(feed_key_for(inactive_user, 'reblogs:2'), 3) redis.sadd(feed_key_for(inactive_user, 'reblogs:2'), 3)
subject.perform
end end
it 'clears feeds of inactive users and lists' do 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(inactive_user))).to eq 0
expect(redis.zcard(feed_key_for(active_user))).to eq 1 expect(redis.zcard(feed_key_for(active_user))).to eq 1
expect(redis.exists?(feed_key_for(inactive_user, 'reblogs'))).to be false 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!(: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) } 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 it 'handles attachments based on metadata details' do
subject.perform
expect(old_remote_media.reload.file) # Remote and past retention period expect(old_remote_media.reload.file) # Remote and past retention period
.to be_blank .to be_blank
expect(old_local_media.reload.file) # Local and past retention expect(old_local_media.reload.file) # Local and past retention

View file

@ -15,24 +15,22 @@ RSpec.describe Vacuum::PreviewCardsVacuum do
before do before do
old_preview_card.statuses << Fabricate(:status) old_preview_card.statuses << Fabricate(:status)
new_preview_card.statuses << Fabricate(:status) new_preview_card.statuses << Fabricate(:status)
end
it 'handles preview card cleanup' do
subject.perform subject.perform
end
it 'deletes cache of preview cards last updated before the retention period' do expect(old_preview_card.reload.image) # last updated before retention period
expect(old_preview_card.reload.image).to be_blank .to be_blank
end
it 'does not delete cache of preview cards last updated within the retention period' do expect(new_preview_card.reload.image) # last updated within the retention period
expect(new_preview_card.reload.image).to_not be_blank .to_not be_blank
end
it 'does not delete attached preview cards' do expect(new_preview_card.reload) # Keep attached preview cards
expect(new_preview_card.reload).to be_persisted .to be_persisted
end
it 'does not delete orphaned preview cards in the retention period' do expect(orphaned_preview_card.reload) # keep orphaned cards in the retention period
expect(orphaned_preview_card.reload).to be_persisted .to be_persisted
end end
end 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_old) { Fabricate(:status, created_at: (retention_period + 2.days).ago) }
let!(:local_status_recent) { 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 subject.perform
end
it 'deletes remote statuses past the retention period' do expect { remote_status_old.reload }
expect { remote_status_old.reload }.to raise_error ActiveRecord::RecordNotFound .to raise_error ActiveRecord::RecordNotFound
end
it 'does not delete local statuses past the retention period' do expect { local_status_old.reload }
expect { local_status_old.reload }.to_not raise_error .to_not raise_error
end
it 'does not delete remote statuses within the retention period' do expect { remote_status_recent.reload }
expect { remote_status_recent.reload }.to_not raise_error .to_not raise_error
end
it 'does not delete local statuses within the retention period' do expect { local_status_recent.reload }
expect { local_status_recent.reload }.to_not raise_error .to_not raise_error
end end
end end
end end

View file

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

View file

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

View file

@ -3,7 +3,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Poll do RSpec.describe Poll do
describe 'scopes' do describe 'Scopes' do
let(:status) { Fabricate(:status) } let(:status) { Fabricate(:status) }
let(:attached_poll) { Fabricate(:poll, status: status) } let(:attached_poll) { Fabricate(:poll, status: status) }
let(:not_attached_poll) do let(:not_attached_poll) do
@ -13,7 +13,7 @@ RSpec.describe Poll do
end end
end end
describe 'attached' do describe '.attached' do
it 'finds the correct records' do it 'finds the correct records' do
results = described_class.attached results = described_class.attached
@ -21,7 +21,7 @@ RSpec.describe Poll do
end end
end end
describe 'unattached' do describe '.unattached' do
it 'finds the correct records' do it 'finds the correct records' do
results = described_class.unattached results = described_class.unattached
@ -30,11 +30,23 @@ RSpec.describe Poll do
end end
end end
describe 'validations' do describe '#reset_votes!' do
context 'when not valid' do let(:poll) { Fabricate :poll, cached_tallies: [2, 3], votes_count: 5, voters_count: 5 }
subject { Fabricate.build(:poll) } 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
end end
describe 'Validations' do
subject { Fabricate.build(:poll) }
it { is_expected.to validate_presence_of(:expires_at) }
end
end end

View file

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

View file

@ -387,23 +387,43 @@ RSpec.describe User do
end end
end end
describe 'token_for_app' do describe '#token_for_app' do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:app) { Fabricate(:application, owner: user) }
it 'returns a token' do context 'when user owns app but does not have tokens' do
expect(user.token_for_app(app)).to be_a(Doorkeeper::AccessToken) 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 end
it 'persists a token' do context 'when user owns app and already has tokens' do
t = user.token_for_app(app) let(:app) { Fabricate(:application, owner: user) }
expect(user.token_for_app(app)).to eql(t) 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 end
it 'is nil if user does not own app' do context 'when user does not own app' do
app.update!(owner: nil) 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
end end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
require 'pundit/rspec'
RSpec.describe CustomEmojiPolicy do RSpec.describe CustomEmojiPolicy do
subject { described_class } 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 # frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
require 'pundit/rspec'
RSpec.describe DeliveryPolicy do RSpec.describe DeliveryPolicy do
let(:policy) { described_class } let(:policy) { described_class }
@ -11,13 +10,13 @@ RSpec.describe DeliveryPolicy do
permissions :clear_delivery_errors?, :restart_delivery?, :stop_delivery? do permissions :clear_delivery_errors?, :restart_delivery?, :stop_delivery? do
context 'with an admin' do context 'with an admin' do
it 'permits' do it 'permits' do
expect(policy).to permit(admin, Tag) expect(policy).to permit(admin, nil)
end end
end end
context 'with a non-admin' do context 'with a non-admin' do
it 'denies' do it 'denies' do
expect(policy).to_not permit(john, Tag) expect(policy).to_not permit(john, nil)
end end
end 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 # frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
require 'pundit/rspec'
RSpec.describe DomainBlockPolicy do RSpec.describe DomainBlockPolicy do
subject { described_class } subject { described_class }
@ -9,7 +8,7 @@ RSpec.describe DomainBlockPolicy do
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) } let(:john) { Fabricate(:account) }
permissions :index?, :show?, :create?, :destroy? do permissions :index?, :show?, :create?, :destroy?, :update? do
context 'when admin' do context 'when admin' do
it 'permits' do it 'permits' do
expect(subject).to permit(admin, DomainBlock) expect(subject).to permit(admin, DomainBlock)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
require 'pundit/rspec'
RSpec.describe UserPolicy do RSpec.describe UserPolicy do
subject { described_class } subject { described_class }
@ -112,4 +111,42 @@ RSpec.describe UserPolicy do
end end
end 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 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 # frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
require 'pundit/rspec'
RSpec.describe WebhookPolicy do RSpec.describe WebhookPolicy do
let(:policy) { described_class } let(:policy) { described_class }

View file

@ -43,6 +43,7 @@ require 'paperclip/matchers'
require 'capybara/rspec' require 'capybara/rspec'
require 'chewy/rspec' require 'chewy/rspec'
require 'email_spec/rspec' require 'email_spec/rspec'
require 'pundit/rspec'
require 'test_prof/recipes/rspec/before_all' require 'test_prof/recipes/rspec/before_all'
Rails.root.glob('spec/support/**/*.rb').each { |f| require f } 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_third) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) } 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({ expect(subject).to include({
'@context' => include('https://www.w3.org/ns/activitystreams'), '@context' => include('https://www.w3.org/ns/activitystreams'),
'type' => 'Note', 'type' => 'Note',
@ -22,26 +22,23 @@ RSpec.describe ActivityPub::NoteSerializer do
'contentMap' => include({ 'contentMap' => include({
'zh-TW' => a_kind_of(String), 'zh-TW' => a_kind_of(String),
}), }),
'replies' => replies_collection_values,
}) })
end end
it 'has a replies collection' do def replies_collection_values
expect(subject['replies']['type']).to eql('Collection') include(
'type' => eql('Collection'),
'first' => include(
'type' => eql('CollectionPage'),
'items' => reply_items
)
)
end end
it 'has a replies collection with a first Page' do def reply_items
expect(subject['replies']['first']['type']).to eql('CollectionPage') include(reply_by_account_first.uri, reply_by_account_next.uri, reply_by_account_third.uri) # Public self replies
end .and(not_include(reply_by_other_first.uri)) # Replies from others
.and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility
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)
end end
end end

View file

@ -18,7 +18,7 @@ RSpec.describe TranslateStatusService do
describe '#call' do describe '#call' do
before do before do
translation_service = TranslationService.new 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| allow(translation_service).to receive(:translate) do |texts|
texts.map do |text| texts.map do |text|
TranslationService::Translation.new( TranslationService::Translation.new(
@ -37,6 +37,7 @@ RSpec.describe TranslateStatusService do
.to have_attributes( .to have_attributes(
content: '<p>Hola</p>', content: '<p>Hola</p>',
detected_source_language: 'en', detected_source_language: 'en',
language: 'es',
provider: 'Dummy', provider: 'Dummy',
status: status status: status
) )
@ -101,6 +102,16 @@ RSpec.describe TranslateStatusService do
expect(media_attachment.description).to eq 'Hola & :highfive:' expect(media_attachment.description).to eq 'Hola & :highfive:'
end end
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 end
describe '#source_texts' do 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 RSpec.describe Scheduler::IpCleanupScheduler do
let(:worker) { described_class.new } let(:worker) { described_class.new }
describe 'perform' do describe '#perform' do
it 'runs without error' do context 'with IP-related data past retention times' do
expect { worker.perform }.to_not raise_error 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 end
end end

View file

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