Merge commit '39110d1d0af5e3d9cf452ae47496a52797249fd0' into glitch-soc/merge-upstream

This commit is contained in:
Claire 2023-06-18 10:36:14 +02:00
commit 65cbcce997
76 changed files with 2108 additions and 1483 deletions

View File

@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` # `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.50.2. # using RuboCop version 1.52.1.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
@ -48,17 +48,14 @@ Layout/SpaceInLambdaLiteral:
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/content_security_policy.rb' - 'config/initializers/content_security_policy.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowedMethods, AllowedPatterns. # Configuration parameters: AllowedMethods, AllowedPatterns.
Lint/AmbiguousBlockAssociation: Lint/AmbiguousBlockAssociation:
Exclude: Exclude:
- 'spec/controllers/admin/account_moderation_notes_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb' - 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/otp_authentication_controller_spec.rb' - 'spec/controllers/settings/two_factor_authentication/otp_authentication_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb'
- 'spec/services/activitypub/process_status_update_service_spec.rb' - 'spec/services/activitypub/process_status_update_service_spec.rb'
- 'spec/services/post_status_service_spec.rb' - 'spec/services/post_status_service_spec.rb'
- 'spec/services/suspend_account_service_spec.rb'
- 'spec/services/unsuspend_account_service_spec.rb'
- 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb'
# Configuration parameters: AllowComments, AllowEmptyLambdas. # Configuration parameters: AllowComments, AllowEmptyLambdas.
@ -124,6 +121,7 @@ Lint/UnusedBlockArgument:
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
- 'config/initializers/simple_form.rb' - 'config/initializers/simple_form.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Lint/UselessAssignment: Lint/UselessAssignment:
Exclude: Exclude:
- 'app/services/activitypub/process_status_update_service.rb' - 'app/services/activitypub/process_status_update_service.rb'
@ -145,6 +143,7 @@ Lint/UselessAssignment:
- 'spec/services/resolve_url_service_spec.rb' - 'spec/services/resolve_url_service_spec.rb'
- 'spec/views/statuses/show.html.haml_spec.rb' - 'spec/views/statuses/show.html.haml_spec.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: CheckForMethodsWithNoSideEffects. # Configuration parameters: CheckForMethodsWithNoSideEffects.
Lint/Void: Lint/Void:
Exclude: Exclude:
@ -167,7 +166,7 @@ Metrics/CyclomaticComplexity:
# Configuration parameters: AllowedMethods, AllowedPatterns. # Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 28 Max: 27
Naming/AccessorMethodName: Naming/AccessorMethodName:
Exclude: Exclude:
@ -180,6 +179,7 @@ Naming/FileName:
Exclude: Exclude:
- 'config/locales/sr-Latn.rb' - 'config/locales/sr-Latn.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyleForLeadingUnderscores. # Configuration parameters: EnforcedStyleForLeadingUnderscores.
# SupportedStylesForLeadingUnderscores: disallowed, required, optional # SupportedStylesForLeadingUnderscores: disallowed, required, optional
Naming/MemoizedInstanceVariableName: Naming/MemoizedInstanceVariableName:
@ -322,8 +322,6 @@ RSpec/LetSetup:
- 'spec/controllers/admin/statuses_controller_spec.rb' - 'spec/controllers/admin/statuses_controller_spec.rb'
- 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb' - 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb'
- 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb'
- 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb'
- 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb'
- 'spec/controllers/api/v1/filters_controller_spec.rb' - 'spec/controllers/api/v1/filters_controller_spec.rb'
- 'spec/controllers/api/v1/followed_tags_controller_spec.rb' - 'spec/controllers/api/v1/followed_tags_controller_spec.rb'
- 'spec/controllers/api/v1/tags_controller_spec.rb' - 'spec/controllers/api/v1/tags_controller_spec.rb'
@ -396,7 +394,7 @@ RSpec/MessageSpies:
- 'spec/validators/status_length_validator_spec.rb' - 'spec/validators/status_length_validator_spec.rb'
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 19 Max: 8
# Configuration parameters: AllowSubject. # Configuration parameters: AllowSubject.
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
@ -424,7 +422,6 @@ RSpec/StubbedMock:
RSpec/SubjectDeclaration: RSpec/SubjectDeclaration:
Exclude: Exclude:
- 'spec/controllers/admin/domain_blocks_controller_spec.rb' - 'spec/controllers/admin/domain_blocks_controller_spec.rb'
- 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb'
- 'spec/models/account_migration_spec.rb' - 'spec/models/account_migration_spec.rb'
- 'spec/models/account_spec.rb' - 'spec/models/account_spec.rb'
- 'spec/models/relationship_filter_spec.rb' - 'spec/models/relationship_filter_spec.rb'
@ -599,7 +596,6 @@ Rails/NegateInclude:
- 'app/models/concerns/attachmentable.rb' - 'app/models/concerns/attachmentable.rb'
- 'app/models/concerns/remotable.rb' - 'app/models/concerns/remotable.rb'
- 'app/models/custom_filter.rb' - 'app/models/custom_filter.rb'
- 'app/models/webhook.rb'
- 'app/services/activitypub/process_status_update_service.rb' - 'app/services/activitypub/process_status_update_service.rb'
- 'app/services/fetch_link_card_service.rb' - 'app/services/fetch_link_card_service.rb'
- 'app/services/search_service.rb' - 'app/services/search_service.rb'
@ -770,11 +766,9 @@ Rails/WhereExists:
- 'app/workers/move_worker.rb' - 'app/workers/move_worker.rb'
- 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb' - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
- 'lib/tasks/tests.rake' - 'lib/tasks/tests.rake'
- 'spec/controllers/api/v1/accounts/notes_controller_spec.rb'
- 'spec/controllers/api/v1/tags_controller_spec.rb' - 'spec/controllers/api/v1/tags_controller_spec.rb'
- 'spec/models/account_spec.rb' - 'spec/models/account_spec.rb'
- 'spec/services/activitypub/process_collection_service_spec.rb' - 'spec/services/activitypub/process_collection_service_spec.rb'
- 'spec/services/post_status_service_spec.rb'
- 'spec/services/purge_domain_service_spec.rb' - 'spec/services/purge_domain_service_spec.rb'
- 'spec/services/unallow_domain_service_spec.rb' - 'spec/services/unallow_domain_service_spec.rb'
@ -796,6 +790,7 @@ Style/ClassVars:
Exclude: Exclude:
- 'config/initializers/devise.rb' - 'config/initializers/devise.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/CombinableLoops: Style/CombinableLoops:
Exclude: Exclude:
- 'app/models/form/custom_emoji_batch.rb' - 'app/models/form/custom_emoji_batch.rb'

View File

@ -470,8 +470,9 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
ox (2.14.16) ox (2.14.16)
parallel (1.23.0) parallel (1.23.0)
parser (3.2.2.1) parser (3.2.2.3)
ast (~> 2.4.1) ast (~> 2.4.1)
racc
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
@ -495,7 +496,7 @@ GEM
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.6.2) racc (1.7.0)
rack (2.2.7) rack (2.2.7)
rack-attack (6.6.1) rack-attack (6.6.1)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
@ -557,7 +558,7 @@ GEM
redis (>= 4) redis (>= 4)
redlock (1.3.2) redlock (1.3.2)
redis (>= 3.0.0, < 6.0) redis (>= 3.0.0, < 6.0)
regexp_parser (2.8.0) regexp_parser (2.8.1)
request_store (1.5.1) request_store (1.5.1)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.0) responders (3.1.0)
@ -591,17 +592,17 @@ GEM
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.12.0) rspec-support (3.12.0)
rspec_chunked (0.6) rspec_chunked (0.6)
rubocop (1.51.0) rubocop (1.52.1)
json (~> 2.3) json (~> 2.3)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.0.0) parser (>= 3.2.2.3)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0) rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.28.1) rubocop-ast (1.29.0)
parser (>= 3.2.1.0) parser (>= 3.2.1.0)
rubocop-capybara (2.18.0) rubocop-capybara (2.18.0)
rubocop (~> 1.41) rubocop (~> 1.41)

View File

@ -2,6 +2,7 @@
module CaptchaConcern module CaptchaConcern
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Hcaptcha::Adapters::ViewMethods include Hcaptcha::Adapters::ViewMethods
included do included do
@ -35,18 +36,22 @@ module CaptchaConcern
flash.delete(:hcaptcha_error) flash.delete(:hcaptcha_error)
yield message yield message
end end
false false
end end
end end
def extend_csp_for_captcha! def extend_csp_for_captcha!
policy = request.content_security_policy policy = request.content_security_policy
return unless captcha_required? && policy.present? return unless captcha_required? && policy.present?
%w(script_src frame_src style_src connect_src).each do |directive| %w(script_src frame_src style_src connect_src).each do |directive|
values = policy.send(directive) values = policy.send(directive)
values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:') values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:')
values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:') values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:')
policy.send(directive, *values) policy.send(directive, *values)
end end
end end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
class MailSubscriptionsController < ApplicationController
layout 'auth'
skip_before_action :require_functional!
before_action :set_body_classes
before_action :set_user
before_action :set_type
def show; end
def create
@user.settings[email_type_from_param] = false
@user.save!
end
private
def set_user
@user = GlobalID::Locator.locate_signed(params[:token], for: 'unsubscribe')
end
def set_body_classes
@body_classes = 'lighter'
end
def set_type
@type = email_type_from_param
end
def email_type_from_param
case params[:type]
when 'follow', 'reblog', 'favourite', 'mention', 'follow_request'
"notification_emails.#{params[:type]}"
else
raise ArgumentError
end
end
end

View File

@ -0,0 +1,27 @@
interface Props {
size: number;
strokeWidth: number;
}
export const CircularProgress: React.FC<Props> = ({ size, strokeWidth }) => {
const viewBox = `0 0 ${size} ${size}`;
const radius = (size - strokeWidth) / 2;
return (
<svg
width={size}
height={size}
viewBox={viewBox}
className='circular-progress'
role='progressbar'
>
<circle
fill='none'
cx={size / 2}
cy={size / 2}
r={radius}
strokeWidth={`${strokeWidth}px`}
/>
</svg>
);
};

View File

@ -8,8 +8,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay'; import Overlay from 'react-overlays/Overlay';
import { CircularProgress } from 'mastodon/components/loading_indicator'; import { CircularProgress } from "./circular_progress";
import { IconButton } from './icon_button'; import { IconButton } from './icon_button';
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;

View File

@ -1,23 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
export default class LoadPending extends PureComponent {
static propTypes = {
onClick: PropTypes.func,
count: PropTypes.number,
};
render() {
const { count } = this.props;
return (
<button className='load-more load-gap' onClick={this.props.onClick}>
<FormattedMessage id='load_pending' defaultMessage='{count, plural, one {# new item} other {# new items}}' values={{ count }} />
</button>
);
}
}

View File

@ -0,0 +1,18 @@
import { FormattedMessage } from 'react-intl';
interface Props {
onClick: (event: React.MouseEvent) => void;
count: number;
}
export const LoadPending: React.FC<Props> = ({ onClick, count }) => {
return (
<button className='load-more load-gap' onClick={onClick}>
<FormattedMessage
id='load_pending'
defaultMessage='{count, plural, one {# new item} other {# new items}}'
values={{ count }}
/>
</button>
);
};

View File

@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
export const CircularProgress = ({ size, strokeWidth }) => {
const viewBox = `0 0 ${size} ${size}`;
const radius = (size - strokeWidth) / 2;
return (
<svg width={size} height={size} viewBox={viewBox} className='circular-progress' role='progressbar'>
<circle
fill='none'
cx={size / 2}
cy={size / 2}
r={radius}
strokeWidth={`${strokeWidth}px`}
/>
</svg>
);
};
CircularProgress.propTypes = {
size: PropTypes.number.isRequired,
strokeWidth: PropTypes.number.isRequired,
};
const LoadingIndicator = () => (
<div className='loading-indicator'>
<CircularProgress size={50} strokeWidth={6} />
</div>
);
export default LoadingIndicator;

View File

@ -0,0 +1,7 @@
import { CircularProgress } from './circular_progress';
export const LoadingIndicator: React.FC = () => (
<div className='loading-indicator'>
<CircularProgress size={50} strokeWidth={6} />
</div>
);

View File

@ -16,8 +16,8 @@ import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { LoadMore } from './load_more'; import { LoadMore } from './load_more';
import LoadPending from './load_pending'; import { LoadPending } from './load_pending';
import LoadingIndicator from './loading_indicator'; import { LoadingIndicator } from './loading_indicator';
const MOUSE_IDLE_DELAY = 300; const MOUSE_IDLE_DELAY = 300;

View File

@ -374,7 +374,7 @@ class Header extends ImmutablePureComponent {
let badge; let badge;
if (account.get('bot')) { if (account.get('bot')) {
badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>); badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Automated' /></div>);
} else if (account.get('group')) { } else if (account.get('group')) {
badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>); badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>);
} else { } else {

View File

@ -10,7 +10,7 @@ import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import ColumnBackButton from 'mastodon/components/column_back_button'; import ColumnBackButton from 'mastodon/components/column_back_button';
import { LoadMore } from 'mastodon/components/load_more'; import { LoadMore } from 'mastodon/components/load_more';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollContainer from 'mastodon/containers/scroll_container'; import ScrollContainer from 'mastodon/containers/scroll_container';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map';

View File

@ -17,7 +17,7 @@ import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { fetchFeaturedTags } from '../../actions/featured_tags'; import { fetchFeaturedTags } from '../../actions/featured_tags';
import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines'; import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';
import LoadingIndicator from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import StatusList from '../../components/status_list'; import StatusList from '../../components/status_list';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View File

@ -10,7 +10,7 @@ import { debounce } from 'lodash';
import { fetchBlocks, expandBlocks } from '../../actions/blocks'; import { fetchBlocks, expandBlocks } from '../../actions/blocks';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import LoadingIndicator from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View File

@ -14,7 +14,7 @@ import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import { LoadMore } from 'mastodon/components/load_more'; import { LoadMore } from 'mastodon/components/load_more';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RadioButton } from 'mastodon/components/radio_button'; import { RadioButton } from 'mastodon/components/radio_button';
import ScrollContainer from 'mastodon/containers/scroll_container'; import ScrollContainer from 'mastodon/containers/scroll_container';

View File

@ -12,7 +12,7 @@ import { debounce } from 'lodash';
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks'; import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import LoadingIndicator from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import DomainContainer from '../../containers/domain_container'; import DomainContainer from '../../containers/domain_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View File

@ -0,0 +1,51 @@
import type { BaseEmoji, EmojiData, NimbleEmojiIndex } from 'emoji-mart';
import type { Category, Data, Emoji } from 'emoji-mart/dist-es/utils/data';
/*
* The 'search' property, although not defined in the [`Emoji`]{@link node_modules/@types/emoji-mart/dist-es/utils/data.d.ts#Emoji} type,
* is used in the application.
* This could be due to an oversight by the library maintainer.
* The `search` property is defined and used [here]{@link node_modules/emoji-mart/dist/utils/data.js#uncompress}.
*/
export type Search = string;
/*
* The 'skins' property does not exist in the application data.
* This could be a potential area of refactoring or error handling.
* The non-existence of 'skins' property is evident at [this location]{@link app/javascript/mastodon/features/emoji/emoji_compressed.js:121}.
*/
export type Skins = null;
export type FilenameData = string[] | string[][];
export type ShortCodesToEmojiDataKey =
| EmojiData['id']
| BaseEmoji['native']
| keyof NimbleEmojiIndex['emojis'];
export type SearchData = [
BaseEmoji['native'],
Emoji['short_names'],
Search,
Emoji['unified']
];
export interface ShortCodesToEmojiData {
[key: ShortCodesToEmojiDataKey]: [FilenameData, SearchData];
}
export type EmojisWithoutShortCodes = FilenameData[];
export type EmojiCompressed = [
ShortCodesToEmojiData,
Skins,
Category[],
Data['aliases'],
EmojisWithoutShortCodes
];
/*
* `emoji_compressed.js` uses `babel-plugin-preval`, which makes it difficult to convert to TypeScript.
* As a temporary solution, we are allowing a default export here to apply the TypeScript type `EmojiCompressed` to the JS file export.
* - {@link app/javascript/mastodon/features/emoji/emoji_compressed.js}
*/
declare const emojiCompressed: EmojiCompressed;
export default emojiCompressed; // eslint-disable-line import/no-default-export

View File

@ -118,6 +118,16 @@ Object.keys(emojiIndex.emojis).forEach(key => {
// inconsistent behavior in dev mode // inconsistent behavior in dev mode
module.exports = JSON.parse(JSON.stringify([ module.exports = JSON.parse(JSON.stringify([
shortCodesToEmojiData, shortCodesToEmojiData,
/*
* The property `skins` is not found in the current context.
* This could potentially lead to issues when interacting with modules or data structures
* that expect the presence of `skins` property.
* Currently, no definitions or references to `skins` property can be found in:
* - {@link node_modules/emoji-mart/dist/utils/data.js}
* - {@link node_modules/emoji-mart/data/all.json}
* - {@link app/javascript/mastodon/features/emoji/emoji_compressed.d.ts#Skins}
* Future refactorings or updates should consider adding definitions or handling for `skins` property.
*/
emojiMartData.skins, emojiMartData.skins,
emojiMartData.categories, emojiMartData.categories,
emojiMartData.aliases, emojiMartData.aliases,

View File

@ -1,43 +0,0 @@
// The output of this module is designed to mimic emoji-mart's
// "data" object, such that we can use it for a light version of emoji-mart's
// emojiIndex.search functionality.
import emojiCompressed from './emoji_compressed';
import { unicodeToUnifiedName } from './unicode_to_unified_name';
const [ shortCodesToEmojiData, skins, categories, short_names ] = emojiCompressed;
const emojis = {};
// decompress
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
let [
filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars
searchData,
] = shortCodesToEmojiData[shortCode];
let [
native,
short_names,
search,
unified,
] = searchData;
if (!unified) {
// unified name can be derived from unicodeToUnifiedName
unified = unicodeToUnifiedName(native);
}
short_names = [shortCode].concat(short_names);
emojis[shortCode] = {
native,
search,
short_names,
unified,
};
});
export {
emojis,
skins,
categories,
short_names,
};

View File

@ -0,0 +1,52 @@
// The output of this module is designed to mimic emoji-mart's
// "data" object, such that we can use it for a light version of emoji-mart's
// emojiIndex.search functionality.
import type { BaseEmoji } from 'emoji-mart';
import type { Emoji } from 'emoji-mart/dist-es/utils/data';
import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
import emojiCompressed from './emoji_compressed';
import { unicodeToUnifiedName } from './unicode_to_unified_name';
type Emojis = {
[key in keyof ShortCodesToEmojiData]: {
native: BaseEmoji['native'];
search: Search;
short_names: Emoji['short_names'];
unified: Emoji['unified'];
};
};
const [
shortCodesToEmojiData,
skins,
categories,
short_names,
_emojisWithoutShortCodes,
] = emojiCompressed;
const emojis: Emojis = {};
// decompress
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
const [_filenameData, searchData] = shortCodesToEmojiData[shortCode];
const native = searchData[0];
let short_names = searchData[1];
const search = searchData[2];
let unified = searchData[3];
if (!unified) {
// unified name can be derived from unicodeToUnifiedName
unified = unicodeToUnifiedName(native);
}
if (short_names) short_names = [shortCode].concat(short_names);
emojis[shortCode] = {
native,
search,
short_names,
unified,
};
});
export { emojis, skins, categories, short_names };

View File

@ -8,7 +8,7 @@ import { connect } from 'react-redux';
import { fetchTrendingLinks } from 'mastodon/actions/trends'; import { fetchTrendingLinks } from 'mastodon/actions/trends';
import DismissableBanner from 'mastodon/components/dismissable_banner'; import DismissableBanner from 'mastodon/components/dismissable_banner';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import Story from './components/story'; import Story from './components/story';

View File

@ -12,7 +12,7 @@ import { connect } from 'react-redux';
import { expandSearch } from 'mastodon/actions/search'; import { expandSearch } from 'mastodon/actions/search';
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
import { LoadMore } from 'mastodon/components/load_more'; import { LoadMore } from 'mastodon/components/load_more';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import Account from 'mastodon/containers/account_container'; import Account from 'mastodon/containers/account_container';
import Status from 'mastodon/containers/status_container'; import Status from 'mastodon/containers/status_container';

View File

@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { fetchSuggestions } from 'mastodon/actions/suggestions'; import { fetchSuggestions } from 'mastodon/actions/suggestions';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import AccountCard from 'mastodon/features/directory/components/account_card'; import AccountCard from 'mastodon/features/directory/components/account_card';
const mapStateToProps = state => ({ const mapStateToProps = state => ({

View File

@ -9,7 +9,7 @@ import { connect } from 'react-redux';
import { fetchTrendingHashtags } from 'mastodon/actions/trends'; import { fetchTrendingHashtags } from 'mastodon/actions/trends';
import DismissableBanner from 'mastodon/components/dismissable_banner'; import DismissableBanner from 'mastodon/components/dismissable_banner';
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
hashtags: state.getIn(['trends', 'tags', 'items']), hashtags: state.getIn(['trends', 'tags', 'items']),

View File

@ -11,7 +11,7 @@ import { connect } from 'react-redux';
import { fetchFavourites } from 'mastodon/actions/interactions'; import { fetchFavourites } from 'mastodon/actions/interactions';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollableList from 'mastodon/components/scrollable_list'; import ScrollableList from 'mastodon/components/scrollable_list';
import AccountContainer from 'mastodon/containers/account_container'; import AccountContainer from 'mastodon/containers/account_container';
import Column from 'mastodon/features/ui/components/column'; import Column from 'mastodon/features/ui/components/column';

View File

@ -20,7 +20,7 @@ import {
expandFollowers, expandFollowers,
} from '../../actions/accounts'; } from '../../actions/accounts';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';
import LoadingIndicator from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; import LimitedAccountHint from '../account_timeline/components/limited_account_hint';

View File

@ -20,7 +20,7 @@ import {
expandFollowing, expandFollowing,
} from '../../actions/accounts'; } from '../../actions/accounts';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';
import LoadingIndicator from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; import LimitedAccountHint from '../account_timeline/components/limited_account_hint';

View File

@ -18,7 +18,7 @@ import { expandListTimeline } from 'mastodon/actions/timelines';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RadioButton } from 'mastodon/components/radio_button'; import { RadioButton } from 'mastodon/components/radio_button';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container'; import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';

View File

@ -12,7 +12,7 @@ import { createSelector } from 'reselect';
import { fetchLists } from 'mastodon/actions/lists'; import { fetchLists } from 'mastodon/actions/lists';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollableList from 'mastodon/components/scrollable_list'; import ScrollableList from 'mastodon/components/scrollable_list';
import ColumnLink from 'mastodon/features/ui/components/column_link'; import ColumnLink from 'mastodon/features/ui/components/column_link';
import ColumnSubheading from 'mastodon/features/ui/components/column_subheading'; import ColumnSubheading from 'mastodon/features/ui/components/column_subheading';

View File

@ -12,7 +12,7 @@ import { debounce } from 'lodash';
import { fetchMutes, expandMutes } from '../../actions/mutes'; import { fetchMutes, expandMutes } from '../../actions/mutes';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import LoadingIndicator from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View File

@ -8,10 +8,12 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { AvatarOverlay } from 'mastodon/components/avatar_overlay'; import { AvatarOverlay } from 'mastodon/components/avatar_overlay';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
// This needs to be kept in sync with app/models/report.rb
const messages = defineMessages({ const messages = defineMessages({
openReport: { id: 'report_notification.open', defaultMessage: 'Open report' }, openReport: { id: 'report_notification.open', defaultMessage: 'Open report' },
other: { id: 'report_notification.categories.other', defaultMessage: 'Other' }, other: { id: 'report_notification.categories.other', defaultMessage: 'Other' },
spam: { id: 'report_notification.categories.spam', defaultMessage: 'Spam' }, spam: { id: 'report_notification.categories.spam', defaultMessage: 'Spam' },
legal: { id: 'report_notification.categories.legal', defaultMessage: 'Legal' },
violation: { id: 'report_notification.categories.violation', defaultMessage: 'Rule violation' }, violation: { id: 'report_notification.categories.violation', defaultMessage: 'Rule violation' },
}); });

View File

@ -12,7 +12,7 @@ import { Icon } from 'mastodon/components/icon';
import { fetchReblogs } from '../../actions/interactions'; import { fetchReblogs } from '../../actions/interactions';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
import LoadingIndicator from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View File

@ -8,7 +8,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Button from 'mastodon/components/button'; import Button from 'mastodon/components/button';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import StatusCheckBox from 'mastodon/features/report/containers/status_check_box_container'; import StatusCheckBox from 'mastodon/features/report/containers/status_check_box_container';
const mapStateToProps = (state, { accountId }) => ({ const mapStateToProps = (state, { accountId }) => ({

View File

@ -14,7 +14,7 @@ import { createSelector } from 'reselect';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollContainer from 'mastodon/containers/scroll_container'; import ScrollContainer from 'mastodon/containers/scroll_container';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';

View File

@ -1,4 +1,4 @@
import LoadingIndicator from '../../../components/loading_indicator'; import { LoadingIndicator } from '../../../components/loading_indicator';
// Keep the markup in sync with <BundleModalError /> // Keep the markup in sync with <BundleModalError />
// (make sure they have the same dimensions) // (make sure they have the same dimensions)

View File

@ -13,7 +13,7 @@
"about.rules": "Server rules", "about.rules": "Server rules",
"account.account_note_header": "Note", "account.account_note_header": "Note",
"account.add_or_remove_from_list": "Add or Remove from lists", "account.add_or_remove_from_list": "Add or Remove from lists",
"account.badges.bot": "Bot", "account.badges.bot": "Automated",
"account.badges.group": "Group", "account.badges.group": "Group",
"account.block": "Block @{name}", "account.block": "Block @{name}",
"account.block_domain": "Block domain {domain}", "account.block_domain": "Block domain {domain}",
@ -553,6 +553,7 @@
"report.unfollow": "Unfollow @{name}", "report.unfollow": "Unfollow @{name}",
"report.unfollow_explanation": "You are following this account. To not see their posts in your home feed anymore, unfollow them.", "report.unfollow_explanation": "You are following this account. To not see their posts in your home feed anymore, unfollow them.",
"report_notification.attached_statuses": "{count, plural, one {{count} post} other {{count} posts}} attached", "report_notification.attached_statuses": "{count, plural, one {{count} post} other {{count} posts}} attached",
"report_notification.categories.legal": "Legal",
"report_notification.categories.other": "Other", "report_notification.categories.other": "Other",
"report_notification.categories.spam": "Spam", "report_notification.categories.spam": "Spam",
"report_notification.categories.violation": "Rule violation", "report_notification.categories.violation": "Rule violation",

View File

@ -1048,7 +1048,9 @@ code {
} }
.simple_form .h-captcha { .simple_form .h-captcha {
text-align: center; display: flex;
justify-content: center;
margin-bottom: 30px;
} }
.permissions-list { .permissions-list {

View File

@ -8,61 +8,71 @@ class NotificationMailer < ApplicationMailer
def mention(recipient, notification) def mention(recipient, notification)
@me = recipient @me = recipient
@user = recipient.user
@type = 'mention'
@status = notification.target_status @status = notification.target_status
return unless @me.user.functional? && @status.present? return unless @user.functional? && @status.present?
locale_for_account(@me) do locale_for_account(@me) do
thread_by_conversation(@status.conversation) thread_by_conversation(@status.conversation)
mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct) mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
end end
end end
def follow(recipient, notification) def follow(recipient, notification)
@me = recipient @me = recipient
@user = recipient.user
@type = 'follow'
@account = notification.from_account @account = notification.from_account
return unless @me.user.functional? return unless @user.functional?
locale_for_account(@me) do locale_for_account(@me) do
mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.follow.subject', name: @account.acct) mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
end end
end end
def favourite(recipient, notification) def favourite(recipient, notification)
@me = recipient @me = recipient
@user = recipient.user
@type = 'favourite'
@account = notification.from_account @account = notification.from_account
@status = notification.target_status @status = notification.target_status
return unless @me.user.functional? && @status.present? return unless @user.functional? && @status.present?
locale_for_account(@me) do locale_for_account(@me) do
thread_by_conversation(@status.conversation) thread_by_conversation(@status.conversation)
mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct) mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
end end
end end
def reblog(recipient, notification) def reblog(recipient, notification)
@me = recipient @me = recipient
@user = recipient.user
@type = 'reblog'
@account = notification.from_account @account = notification.from_account
@status = notification.target_status @status = notification.target_status
return unless @me.user.functional? && @status.present? return unless @user.functional? && @status.present?
locale_for_account(@me) do locale_for_account(@me) do
thread_by_conversation(@status.conversation) thread_by_conversation(@status.conversation)
mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct) mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
end end
end end
def follow_request(recipient, notification) def follow_request(recipient, notification)
@me = recipient @me = recipient
@user = recipient.user
@type = 'follow_request'
@account = notification.from_account @account = notification.from_account
return unless @me.user.functional? return unless @user.functional?
locale_for_account(@me) do locale_for_account(@me) do
mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct) mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
end end
end end

View File

@ -48,6 +48,7 @@ class Report < ApplicationRecord
validate :validate_rule_ids validate :validate_rule_ids
# entries here needs to be kept in sync with app/javascript/mastodon/features/notifications/components/report.jsx
enum category: { enum category: {
other: 0, other: 0,
spam: 1_000, spam: 1_000,

View File

@ -7,10 +7,12 @@
= hidden_field_tag :confirmation_token, params[:confirmation_token] = hidden_field_tag :confirmation_token, params[:confirmation_token]
= hidden_field_tag :redirect_to_app, params[:redirect_to_app] = hidden_field_tag :redirect_to_app, params[:redirect_to_app]
%h1.title= t('auth.captcha_confirmation.title')
%p.lead= t('auth.captcha_confirmation.hint_html') %p.lead= t('auth.captcha_confirmation.hint_html')
.field-group
= render_captcha = render_captcha
%p.lead= t('auth.captcha_confirmation.help_html', email: mail_to(Setting.site_contact_email, nil))
.actions .actions
%button.button= t('challenge.confirm') = button_tag t('challenge.confirm'), class: 'button', type: :submit

View File

@ -44,7 +44,11 @@
%tbody %tbody
%td.column-cell %td.column-cell
%p= t 'about.hosted_on', domain: site_hostname %p= t 'about.hosted_on', domain: site_hostname
%p= link_to t('application_mailer.notification_preferences'), settings_preferences_notifications_url %p
= link_to t('application_mailer.notification_preferences'), settings_preferences_notifications_url
- if defined?(@type)
·
= link_to t('application_mailer.unsubscribe'), unsubscribe_url(token: @user.to_sgid(for: 'unsubscribe').to_s, type: @type)
%td.column-cell.text-right %td.column-cell.text-right
= link_to root_url do = link_to root_url do
= image_tag full_pack_url('media/images/mailer/logo.png'), alt: 'Mastodon', height: 24 = image_tag full_pack_url('media/images/mailer/logo.png'), alt: 'Mastodon', height: 24

View File

@ -0,0 +1,9 @@
- content_for :page_title do
= t('mail_subscriptions.unsubscribe.title')
.simple_form
%h1.title= t('mail_subscriptions.unsubscribe.complete')
%p.lead
= t('mail_subscriptions.unsubscribe.success_html', domain: content_tag(:strong, site_hostname), type: content_tag(:strong, I18n.t(@type, scope: 'mail_subscriptions.unsubscribe.emails')), email: content_tag(:strong, @user.email))
%p.lead
= t('mail_subscriptions.unsubscribe.resubscribe_html', settings_path: settings_preferences_notifications_path)

View File

@ -0,0 +1,12 @@
- content_for :page_title do
= t('mail_subscriptions.unsubscribe.title')
.simple_form
%h1.title= t('mail_subscriptions.unsubscribe.title')
%p.lead
= t('mail_subscriptions.unsubscribe.confirmation_html', domain: content_tag(:strong, site_hostname), type: content_tag(:strong, I18n.t(@type, scope: 'mail_subscriptions.unsubscribe.emails')), email: content_tag(:strong, @user.email), settings_path: settings_preferences_notifications_path)
= form_tag unsubscribe_path, method: :post do
= hidden_field_tag :token, params[:token]
= hidden_field_tag :type, params[:type]
= button_tag t('mail_subscriptions.unsubscribe.action'), type: :submit

View File

@ -74,6 +74,7 @@ ignore_unused:
- 'notification_mailer.*' - 'notification_mailer.*'
- 'imports.overwrite_preambles.{following,blocking,muting,domain_blocking,bookmarks}_html' - 'imports.overwrite_preambles.{following,blocking,muting,domain_blocking,bookmarks}_html'
- 'imports.preambles.{following,blocking,muting,domain_blocking,bookmarks}_html' - 'imports.preambles.{following,blocking,muting,domain_blocking,bookmarks}_html'
- 'mail_subscriptions.unsubscribe.emails.*'
ignore_inconsistent_interpolations: ignore_inconsistent_interpolations:
- '*.one' - '*.one'

View File

@ -978,6 +978,7 @@ en:
notification_preferences: Change e-mail preferences notification_preferences: Change e-mail preferences
salutation: "%{name}," salutation: "%{name},"
settings: 'Change e-mail preferences: %{link}' settings: 'Change e-mail preferences: %{link}'
unsubscribe: Unsubscribe
view: 'View:' view: 'View:'
view_profile: View profile view_profile: View profile
view_status: View post view_status: View post
@ -992,8 +993,9 @@ en:
auth: auth:
apply_for_account: Request an account apply_for_account: Request an account
captcha_confirmation: captcha_confirmation:
hint_html: Just one more step! To confirm your account, this server requires you to solve a CAPTCHA. You can <a href="/about/more">contact the server administrator</a> if you have questions or need assistance with confirming your account. help_html: If you have issues solving the CAPTCHA, you can get in touch with us through %{email} and we can assist you.
title: User verification hint_html: Just one more thing! We need to confirm you're a human (this is so we can keep the spam out!). Solve the CAPTCHA below and click "Continue".
title: Security check
change_password: Password change_password: Password
confirmations: confirmations:
wrong_email_hint: If that e-mail address is not correct, you can change it in account settings. wrong_email_hint: If that e-mail address is not correct, you can change it in account settings.
@ -1342,6 +1344,21 @@ en:
failed_sign_in_html: Failed sign-in attempt with %{method} from %{ip} (%{browser}) failed_sign_in_html: Failed sign-in attempt with %{method} from %{ip} (%{browser})
successful_sign_in_html: Successful sign-in with %{method} from %{ip} (%{browser}) successful_sign_in_html: Successful sign-in with %{method} from %{ip} (%{browser})
title: Authentication history title: Authentication history
mail_subscriptions:
unsubscribe:
action: Yes, unsubscribe
complete: Unsubscribed
confirmation_html: Are you sure you want to unsubscribe from receiving %{type} for Mastodon on %{domain} to your e-mail at %{email}? You can always re-subscribe from your <a href="%{settings_path}">e-mail notification settings</a>.
emails:
notification_emails:
favourite: favorite notification e-mails
follow: follow notification e-mails
follow_request: follow request e-mails
mention: mention notification e-mails
reblog: boost notification e-mails
resubscribe_html: If you've unsubscribed by mistake, you can re-subscribe from your <a href="%{settings_path}">e-mail notification settings</a>.
success_html: You'll no longer receive %{type} for Mastodon on %{domain} to your e-mail at %{email}.
title: Unsubscribe
media_attachments: media_attachments:
validations: validations:
images_and_video: Cannot attach a video to a post that already contains images images_and_video: Cannot attach a video to a post that already contains images

View File

@ -168,7 +168,7 @@ en:
defaults: defaults:
autofollow: Invite to follow your account autofollow: Invite to follow your account
avatar: Avatar avatar: Avatar
bot: This is a bot account bot: This is an automated account
chosen_languages: Filter languages chosen_languages: Filter languages
confirm_new_password: Confirm new password confirm_new_password: Confirm new password
confirm_password: Confirm password confirm_password: Confirm password

View File

@ -68,6 +68,8 @@ Rails.application.routes.draw do
devise_scope :user do devise_scope :user do
get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite
resource :unsubscribe, only: [:show, :create], controller: :mail_subscriptions
namespace :auth do namespace :auth do
resource :setup, only: [:show, :update], controller: :setup resource :setup, only: [:show, :update], controller: :setup
resource :challenge, only: [:create], controller: :challenges resource :challenge, only: [:create], controller: :challenges

View File

@ -23,22 +23,12 @@ module Mastodon::CLI
def recount(type) def recount(type)
case type case type
when 'accounts' when 'accounts'
processed, = parallelize_with_progress(Account.local.includes(:account_stat)) do |account| processed, = parallelize_with_progress(accounts_with_stats) do |account|
account_stat = account.account_stat recount_account_stats(account)
account_stat.following_count = account.active_relationships.count
account_stat.followers_count = account.passive_relationships.count
account_stat.statuses_count = account.statuses.where.not(visibility: :direct).count
account_stat.save if account_stat.changed?
end end
when 'statuses' when 'statuses'
processed, = parallelize_with_progress(Status.includes(:status_stat)) do |status| processed, = parallelize_with_progress(statuses_with_stats) do |status|
status_stat = status.status_stat recount_status_stats(status)
status_stat.replies_count = status.replies.where.not(visibility: :direct).count
status_stat.reblogs_count = status.reblogs.count
status_stat.favourites_count = status.favourites.count
status_stat.save if status_stat.changed?
end end
else else
say("Unknown type: #{type}", :red) say("Unknown type: #{type}", :red)
@ -48,5 +38,35 @@ module Mastodon::CLI
say say
say("OK, recounted #{processed} records", :green) say("OK, recounted #{processed} records", :green)
end end
private
def accounts_with_stats
Account.local.includes(:account_stat)
end
def statuses_with_stats
Status.includes(:status_stat)
end
def recount_account_stats(account)
account.account_stat.tap do |account_stat|
account_stat.following_count = account.active_relationships.count
account_stat.followers_count = account.passive_relationships.count
account_stat.statuses_count = account.statuses.where.not(visibility: :direct).count
account_stat.save if account_stat.changed?
end
end
def recount_status_stats(status)
status.status_stat.tap do |status_stat|
status_stat.replies_count = status.replies.where.not(visibility: :direct).count
status_stat.reblogs_count = status.reblogs.count
status_stat.favourites_count = status.favourites.count
status_stat.save if status_stat.changed?
end
end
end end
end end

View File

@ -19,7 +19,7 @@ module Mastodon::CLI
LONG_DESC LONG_DESC
def build(username = nil) def build(username = nil)
if options[:all] || username.nil? if options[:all] || username.nil?
processed, = parallelize_with_progress(Account.joins(:user).merge(User.active)) do |account| processed, = parallelize_with_progress(active_user_accounts) do |account|
PrecomputeFeedService.new.call(account) unless dry_run? PrecomputeFeedService.new.call(account) unless dry_run?
end end
@ -47,5 +47,11 @@ module Mastodon::CLI
redis.del(keys) redis.del(keys)
say('OK', :green) say('OK', :green)
end end
private
def active_user_accounts
Account.joins(:user).merge(User.active)
end
end end
end end

View File

@ -48,10 +48,14 @@ describe Api::V1::Accounts::RelationshipsController do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns JSON with correct data' do context 'when there is returned JSON data' do
json = body_as_json let(:json) { body_as_json }
it 'returns an enumerable json' do
expect(json).to be_a Enumerable expect(json).to be_a Enumerable
end
it 'returns a correct first element' do
expect(json.first[:id]).to eq simon.id.to_s expect(json.first[:id]).to eq simon.id.to_s
expect(json.first[:following]).to be true expect(json.first[:following]).to be true
expect(json.first[:showing_reblogs]).to be true expect(json.first[:showing_reblogs]).to be true
@ -59,7 +63,9 @@ describe Api::V1::Accounts::RelationshipsController do
expect(json.first[:muting]).to be false expect(json.first[:muting]).to be false
expect(json.first[:requested]).to be false expect(json.first[:requested]).to be false
expect(json.first[:domain_blocking]).to be false expect(json.first[:domain_blocking]).to be false
end
it 'returns a correct second element' do
expect(json.second[:id]).to eq lewis.id.to_s expect(json.second[:id]).to eq lewis.id.to_s
expect(json.second[:following]).to be false expect(json.second[:following]).to be false
expect(json.second[:showing_reblogs]).to be false expect(json.second[:showing_reblogs]).to be false
@ -68,6 +74,7 @@ describe Api::V1::Accounts::RelationshipsController do
expect(json.second[:requested]).to be false expect(json.second[:requested]).to be false
expect(json.second[:domain_blocking]).to be false expect(json.second[:domain_blocking]).to be false
end end
end
it 'returns JSON with correct data on cached requests too' do it 'returns JSON with correct data on cached requests too' do
get :index, params: { id: [simon.id] } get :index, params: { id: [simon.id] }

View File

@ -55,20 +55,6 @@ RSpec.describe Api::V1::AccountsController do
end end
end end
describe 'GET #show' do
let(:scopes) { 'read:accounts' }
before do
get :show, params: { id: user.account.id }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
end
describe 'POST #follow' do describe 'POST #follow' do
let(:scopes) { 'write:follows' } let(:scopes) { 'write:follows' }
let(:other_account) { Fabricate(:account, username: 'bob', locked: locked) } let(:other_account) { Fabricate(:account, username: 'bob', locked: locked) }

View File

@ -1,358 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Admin::CanonicalEmailBlocksController do
render_views
let(:role) { UserRole.find_by(name: 'Admin') }
let(:user) { Fabricate(:user, role: role) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:scopes) { 'admin:read:canonical_email_blocks admin:write:canonical_email_blocks' }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
describe 'GET #index' do
context 'with wrong scope' do
before do
get :index
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
get :index
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do
get :index
expect(response).to have_http_status(200)
end
context 'when there is no canonical email block' do
it 'returns an empty list' do
get :index
body = body_as_json
expect(body).to be_empty
end
end
context 'when there are canonical email blocks' do
let!(:canonical_email_blocks) { Fabricate.times(5, :canonical_email_block) }
let(:expected_email_hashes) { canonical_email_blocks.pluck(:canonical_email_hash) }
it 'returns the correct canonical email hashes' do
get :index
json = body_as_json
expect(json.pluck(:canonical_email_hash)).to match_array(expected_email_hashes)
end
context 'with limit param' do
let(:params) { { limit: 2 } }
it 'returns only the requested number of canonical email blocks' do
get :index, params: params
json = body_as_json
expect(json.size).to eq(params[:limit])
end
end
context 'with since_id param' do
let(:params) { { since_id: canonical_email_blocks[1].id } }
it 'returns only the canonical email blocks after since_id' do
get :index, params: params
canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s)
json = body_as_json
expect(json.pluck(:id)).to match_array(canonical_email_blocks_ids[2..])
end
end
context 'with max_id param' do
let(:params) { { max_id: canonical_email_blocks[3].id } }
it 'returns only the canonical email blocks before max_id' do
get :index, params: params
canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s)
json = body_as_json
expect(json.pluck(:id)).to match_array(canonical_email_blocks_ids[..2])
end
end
end
end
describe 'GET #show' do
let!(:canonical_email_block) { Fabricate(:canonical_email_block) }
let(:params) { { id: canonical_email_block.id } }
context 'with wrong scope' do
before do
get :show, params: params
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
get :show, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
context 'when canonical email block exists' do
it 'returns http success' do
get :show, params: params
expect(response).to have_http_status(200)
end
it 'returns canonical email block data correctly' do
get :show, params: params
json = body_as_json
expect(json[:id]).to eq(canonical_email_block.id.to_s)
expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
end
end
context 'when canonical block does not exist' do
it 'returns http not found' do
get :show, params: { id: 0 }
expect(response).to have_http_status(404)
end
end
end
describe 'POST #test' do
context 'with wrong scope' do
before do
post :test
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
post :test, params: { email: 'whatever@email.com' }
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
context 'when required email is not provided' do
it 'returns http bad request' do
post :test
expect(response).to have_http_status(400)
end
end
context 'when required email is provided' do
let(:params) { { email: 'example@email.com' } }
context 'when there is a matching canonical email block' do
let!(:canonical_email_block) { CanonicalEmailBlock.create(params) }
it 'returns http success' do
post :test, params: params
expect(response).to have_http_status(200)
end
it 'returns expected canonical email hash' do
post :test, params: params
json = body_as_json
expect(json[0][:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
end
end
context 'when there is no matching canonical email block' do
it 'returns http success' do
post :test, params: params
expect(response).to have_http_status(200)
end
it 'returns an empty list' do
post :test, params: params
json = body_as_json
expect(json).to be_empty
end
end
end
end
describe 'POST #create' do
let(:params) { { email: 'example@email.com' } }
let(:canonical_email_block) { CanonicalEmailBlock.new(email: params[:email]) }
context 'with wrong scope' do
before do
post :create, params: params
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
post :create, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do
post :create, params: params
expect(response).to have_http_status(200)
end
it 'returns canonical_email_hash correctly' do
post :create, params: params
json = body_as_json
expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
end
context 'when required email param is not provided' do
it 'returns http unprocessable entity' do
post :create
expect(response).to have_http_status(422)
end
end
context 'when canonical_email_hash param is provided instead of email' do
let(:params) { { canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } }
it 'returns http success' do
post :create, params: params
expect(response).to have_http_status(200)
end
it 'returns correct canonical_email_hash' do
post :create, params: params
json = body_as_json
expect(json[:canonical_email_hash]).to eq(params[:canonical_email_hash])
end
end
context 'when both email and canonical_email_hash params are provided' do
let(:params) { { email: 'example@email.com', canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } }
it 'returns http success' do
post :create, params: params
expect(response).to have_http_status(200)
end
it 'ignores canonical_email_hash param' do
post :create, params: params
json = body_as_json
expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
end
end
context 'when canonical email was already blocked' do
before do
canonical_email_block.save
end
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
end
describe 'DELETE #destroy' do
let!(:canonical_email_block) { Fabricate(:canonical_email_block) }
let(:params) { { id: canonical_email_block.id } }
context 'with wrong scope' do
before do
delete :destroy, params: params
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
delete :destroy, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do
delete :destroy, params: params
expect(response).to have_http_status(200)
end
context 'when canonical email block is not found' do
it 'returns http not found' do
delete :destroy, params: { id: 0 }
expect(response).to have_http_status(404)
end
end
end
end

View File

@ -1,140 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Api::V1::Admin::DomainAllowsController do
render_views
let(:role) { UserRole.find_by(name: 'Admin') }
let(:user) { Fabricate(:user, role: role) }
let(:scopes) { 'admin:read admin:write' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
describe 'GET #index' do
let!(:domain_allow) { Fabricate(:domain_allow) }
before do
get :index
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns the expected domain allows' do
json = body_as_json
expect(json.length).to eq 1
expect(json[0][:id].to_i).to eq domain_allow.id
end
end
describe 'GET #show' do
let!(:domain_allow) { Fabricate(:domain_allow) }
before do
get :show, params: { id: domain_allow.id }
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns expected domain name' do
json = body_as_json
expect(json[:domain]).to eq domain_allow.domain
end
end
describe 'DELETE #destroy' do
let!(:domain_allow) { Fabricate(:domain_allow) }
before do
delete :destroy, params: { id: domain_allow.id }
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'deletes the block' do
expect(DomainAllow.find_by(id: domain_allow.id)).to be_nil
end
end
describe 'POST #create' do
let!(:domain_allow) { Fabricate(:domain_allow, domain: 'example.com') }
context 'with a valid domain' do
before do
post :create, params: { domain: 'foo.bar.com' }
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns expected domain name' do
json = body_as_json
expect(json[:domain]).to eq 'foo.bar.com'
end
it 'creates a domain block' do
expect(DomainAllow.find_by(domain: 'foo.bar.com')).to_not be_nil
end
end
context 'with invalid domain name' do
before do
post :create, params: { domain: 'foo bar' }
end
it 'returns http unprocessable entity' do
expect(response).to have_http_status(422)
end
end
context 'when domain name is not specified' do
it 'returns http unprocessable entity' do
post :create
expect(response).to have_http_status(422)
end
end
end
end

View File

@ -1,180 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Api::V1::Admin::DomainBlocksController do
render_views
let(:role) { UserRole.find_by(name: 'Admin') }
let(:user) { Fabricate(:user, role: role) }
let(:scopes) { 'admin:read admin:write' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
describe 'GET #index' do
let!(:block) { Fabricate(:domain_block) }
before do
get :index
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns the expected domain blocks' do
json = body_as_json
expect(json.length).to eq 1
expect(json[0][:id].to_i).to eq block.id
end
end
describe 'GET #show' do
let!(:block) { Fabricate(:domain_block) }
before do
get :show, params: { id: block.id }
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns expected domain name' do
json = body_as_json
expect(json[:domain]).to eq block.domain
end
end
describe 'PUT #update' do
let!(:remote_account) { Fabricate(:account, domain: 'example.com') }
let(:subject) do
post :update, params: { id: domain_block.id, domain: 'example.com', severity: new_severity }
end
let(:domain_block) { Fabricate(:domain_block, domain: 'example.com', severity: original_severity) }
before do
BlockDomainService.new.call(domain_block)
end
context 'when downgrading a domain suspension to silence' do
let(:original_severity) { 'suspend' }
let(:new_severity) { 'silence' }
it 'changes the block severity' do
expect { subject }.to change { domain_block.reload.severity }.from('suspend').to('silence')
end
it 'undoes individual suspensions' do
expect { subject }.to change { remote_account.reload.suspended? }.from(true).to(false)
end
it 'performs individual silences' do
expect { subject }.to change { remote_account.reload.silenced? }.from(false).to(true)
end
end
context 'when upgrading a domain silence to suspend' do
let(:original_severity) { 'silence' }
let(:new_severity) { 'suspend' }
it 'changes the block severity' do
expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend')
end
it 'undoes individual silences' do
expect { subject }.to change { remote_account.reload.silenced? }.from(true).to(false)
end
it 'performs individual suspends' do
expect { subject }.to change { remote_account.reload.suspended? }.from(false).to(true)
end
end
end
describe 'DELETE #destroy' do
let!(:block) { Fabricate(:domain_block) }
before do
delete :destroy, params: { id: block.id }
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'deletes the block' do
expect(DomainBlock.find_by(id: block.id)).to be_nil
end
end
describe 'POST #create' do
let(:existing_block_domain) { 'example.com' }
let!(:block) { Fabricate(:domain_block, domain: existing_block_domain, severity: :suspend) }
before do
post :create, params: { domain: 'foo.bar.com', severity: :silence }
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns expected domain name' do
json = body_as_json
expect(json[:domain]).to eq 'foo.bar.com'
end
it 'creates a domain block' do
expect(DomainBlock.find_by(domain: 'foo.bar.com')).to_not be_nil
end
context 'when a stricter domain block already exists' do
let(:existing_block_domain) { 'bar.com' }
it 'returns http unprocessable entity' do
expect(response).to have_http_status(422)
end
it 'renders existing domain block in error' do
json = body_as_json
expect(json[:existing_domain_block][:domain]).to eq existing_block_domain
end
end
end
end

View File

@ -1,309 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Admin::IpBlocksController do
render_views
let(:role) { UserRole.find_by(name: 'Admin') }
let(:user) { Fabricate(:user, role: role) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:scopes) { 'admin:read:ip_blocks admin:write:ip_blocks' }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
describe 'GET #index' do
context 'with wrong scope' do
before do
get :index
end
it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks'
end
context 'with wrong role' do
before do
get :index
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do
get :index
expect(response).to have_http_status(200)
end
context 'when there is no ip block' do
it 'returns an empty body' do
get :index
json = body_as_json
expect(json).to be_empty
end
end
context 'when there are ip blocks' do
let!(:ip_blocks) do
[
IpBlock.create(ip: '192.0.2.0/24', severity: :no_access),
IpBlock.create(ip: '172.16.0.1', severity: :sign_up_requires_approval, comment: 'Spam'),
IpBlock.create(ip: '2001:0db8::/32', severity: :sign_up_block, expires_in: 10.days),
]
end
let(:expected_response) do
ip_blocks.map do |ip_block|
{
id: ip_block.id.to_s,
ip: ip_block.ip,
severity: ip_block.severity.to_s,
comment: ip_block.comment,
created_at: ip_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
expires_at: ip_block.expires_at&.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
}
end
end
it 'returns the correct blocked ips' do
get :index
json = body_as_json
expect(json).to match_array(expected_response)
end
context 'with limit param' do
let(:params) { { limit: 2 } }
it 'returns only the requested number of ip blocks' do
get :index, params: params
json = body_as_json
expect(json.size).to eq(params[:limit])
end
end
end
end
describe 'GET #show' do
let!(:ip_block) { IpBlock.create(ip: '192.0.2.0/24', severity: :no_access) }
let(:params) { { id: ip_block.id } }
context 'with wrong scope' do
before do
get :show, params: params
end
it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks'
end
context 'with wrong role' do
before do
get :show, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do
get :show, params: params
expect(response).to have_http_status(200)
end
it 'returns the correct ip block' do
get :show, params: params
json = body_as_json
expect(json[:ip]).to eq("#{ip_block.ip}/#{ip_block.ip.prefix}")
expect(json[:severity]).to eq(ip_block.severity.to_s)
end
context 'when ip block does not exist' do
it 'returns http not found' do
get :show, params: { id: 0 }
expect(response).to have_http_status(404)
end
end
end
describe 'POST #create' do
let(:params) { { ip: '151.0.32.55', severity: 'no_access', comment: 'Spam' } }
context 'with wrong scope' do
before do
post :create, params: params
end
it_behaves_like 'forbidden for wrong scope', 'admin:read:ip_blocks'
end
context 'with wrong role' do
before do
post :create, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do
post :create, params: params
expect(response).to have_http_status(200)
end
it 'returns the correct ip block' do
post :create, params: params
json = body_as_json
expect(json[:ip]).to eq("#{params[:ip]}/32")
expect(json[:severity]).to eq(params[:severity])
expect(json[:comment]).to eq(params[:comment])
end
context 'when ip is not provided' do
let(:params) { { ip: '', severity: 'no_access' } }
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
context 'when severity is not provided' do
let(:params) { { ip: '173.65.23.1', severity: '' } }
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
context 'when provided ip is already blocked' do
before do
IpBlock.create(params)
end
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
context 'when provided ip address is invalid' do
let(:params) { { ip: '520.13.54.120', severity: 'no_access' } }
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
end
describe 'PUT #update' do
context 'when ip block exists' do
let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access', comment: 'Spam', expires_in: 48.hours) }
let(:params) { { id: ip_block.id, severity: 'sign_up_requires_approval', comment: 'Decreasing severity' } }
it 'returns http success' do
put :update, params: params
expect(response).to have_http_status(200)
end
it 'returns the correct ip block' do
put :update, params: params
json = body_as_json
expect(json).to match(hash_including({
ip: "#{ip_block.ip}/#{ip_block.ip.prefix}",
severity: 'sign_up_requires_approval',
comment: 'Decreasing severity',
}))
end
it 'updates the severity correctly' do
expect { put :update, params: params }.to change { ip_block.reload.severity }.from('no_access').to('sign_up_requires_approval')
end
it 'updates the comment correctly' do
expect { put :update, params: params }.to change { ip_block.reload.comment }.from('Spam').to('Decreasing severity')
end
end
context 'when ip block does not exist' do
it 'returns http not found' do
put :update, params: { id: 0 }
expect(response).to have_http_status(404)
end
end
end
describe 'DELETE #destroy' do
context 'when ip block exists' do
let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access') }
let(:params) { { id: ip_block.id } }
it 'returns http success' do
delete :destroy, params: params
expect(response).to have_http_status(200)
end
it 'returns an empty body' do
delete :destroy, params: params
json = body_as_json
expect(json).to be_empty
end
it 'deletes the ip block' do
delete :destroy, params: params
expect(IpBlock.find_by(id: ip_block.id)).to be_nil
end
end
context 'when ip block does not exist' do
it 'returns http not found' do
delete :destroy, params: { id: 0 }
expect(response).to have_http_status(404)
end
end
end
end

View File

@ -1,111 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Api::V1::Admin::ReportsController do
render_views
let(:role) { UserRole.find_by(name: 'Moderator') }
let(:user) { Fabricate(:user, role: role) }
let(:scopes) { 'admin:read admin:write' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:report) { Fabricate(:report) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
describe 'GET #index' do
before do
get :index
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
end
end
describe 'GET #show' do
before do
get :show, params: { id: report.id }
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
end
end
describe 'POST #resolve' do
before do
post :resolve, params: { id: report.id }
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
end
end
describe 'POST #reopen' do
before do
post :reopen, params: { id: report.id }
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
end
end
describe 'POST #assign_to_self' do
before do
post :assign_to_self, params: { id: report.id }
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
end
end
describe 'POST #unassign' do
before do
post :unassign, params: { id: report.id }
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
end
end
end

View File

@ -56,18 +56,11 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
end end
describe 'when creation succeeds' do describe 'when creation succeeds' do
let!(:otp_backup_codes) { user.generate_otp_backup_codes! }
it 'renders page with success' do it 'renders page with success' do
otp_backup_codes = user.generate_otp_backup_codes! prepare_user_otp_generation
expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value| prepare_user_otp_consumption
expect(value).to eq user
otp_backup_codes
end
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, code, options|
expect(value).to eq user
expect(code).to eq '123456'
expect(options).to eq({ otp_secret: 'thisisasecretforthespecofnewview' })
true
end
expect do expect do
post :create, post :create,
@ -80,6 +73,22 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index') expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
end end
def prepare_user_otp_generation
expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value|
expect(value).to eq user
otp_backup_codes
end
end
def prepare_user_otp_consumption
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, code, options|
expect(value).to eq user
expect(code).to eq '123456'
expect(options).to eq({ otp_secret: 'thisisasecretforthespecofnewview' })
true
end
end
end end
describe 'when creation fails' do describe 'when creation fails' do

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
Fabricator(:domain_allow) do Fabricator(:domain_allow) do
domain 'MyString' domain { sequence(:domain) { |i| "example#{i}.com" } }
end end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
Fabricator(:status_stat) do
status
replies_count '123'
reblogs_count '456'
favourites_count '789'
end

View File

@ -4,9 +4,68 @@ require 'rails_helper'
require 'mastodon/cli/cache' require 'mastodon/cli/cache'
describe Mastodon::CLI::Cache do describe Mastodon::CLI::Cache do
let(:cli) { described_class.new }
describe '.exit_on_failure?' do describe '.exit_on_failure?' do
it 'returns true' do it 'returns true' do
expect(described_class.exit_on_failure?).to be true expect(described_class.exit_on_failure?).to be true
end end
end end
describe '#clear' do
before { allow(Rails.cache).to receive(:clear) }
it 'clears the Rails cache' do
expect { cli.invoke(:clear) }.to output(
a_string_including('OK')
).to_stdout
expect(Rails.cache).to have_received(:clear)
end
end
describe '#recount' do
context 'with the `accounts` argument' do
let(:arguments) { ['accounts'] }
let(:account_stat) { Fabricate(:account_stat) }
before do
account_stat.update(statuses_count: 123)
end
it 're-calculates account records in the cache' do
expect { cli.invoke(:recount, arguments) }.to output(
a_string_including('OK')
).to_stdout
expect(account_stat.reload.statuses_count).to be_zero
end
end
context 'with the `statuses` argument' do
let(:arguments) { ['statuses'] }
let(:status_stat) { Fabricate(:status_stat) }
before do
status_stat.update(replies_count: 123)
end
it 're-calculates account records in the cache' do
expect { cli.invoke(:recount, arguments) }.to output(
a_string_including('OK')
).to_stdout
expect(status_stat.reload.replies_count).to be_zero
end
end
context 'with an unknown type' do
let(:arguments) { ['other-type'] }
it 'Exits with an error message' do
expect { cli.invoke(:recount, arguments) }.to output(
a_string_including('Unknown')
).to_stdout.and raise_error(SystemExit)
end
end
end
end end

View File

@ -4,9 +4,65 @@ require 'rails_helper'
require 'mastodon/cli/feeds' require 'mastodon/cli/feeds'
describe Mastodon::CLI::Feeds do describe Mastodon::CLI::Feeds do
let(:cli) { described_class.new }
describe '.exit_on_failure?' do describe '.exit_on_failure?' do
it 'returns true' do it 'returns true' do
expect(described_class.exit_on_failure?).to be true expect(described_class.exit_on_failure?).to be true
end end
end end
describe '#build' do
before { Fabricate(:account) }
context 'with --all option' do
let(:options) { { all: true } }
it 'regenerates feeds for all accounts' do
expect { cli.invoke(:build, [], options) }.to output(
a_string_including('Regenerated feeds')
).to_stdout
end
end
context 'with a username' do
before { Fabricate(:account, username: 'alice') }
let(:arguments) { ['alice'] }
it 'regenerates feeds for the account' do
expect { cli.invoke(:build, arguments) }.to output(
a_string_including('OK')
).to_stdout
end
end
context 'with invalid username' do
let(:arguments) { ['invalid-username'] }
it 'displays an error and exits' do
expect { cli.invoke(:build, arguments) }.to output(
a_string_including('No such account')
).to_stdout.and raise_error(SystemExit)
end
end
end
describe '#clear' do
before do
allow(redis).to receive(:del).with(key_namespace)
end
it 'clears the redis `feed:*` namespace' do
expect { cli.invoke(:clear) }.to output(
a_string_including('OK')
).to_stdout
expect(redis).to have_received(:del).with(key_namespace).once
end
def key_namespace
redis.keys('feed:*')
end
end
end end

View File

@ -245,19 +245,46 @@ RSpec.describe Form::Import do
expect(account.bulk_imports.first.rows.pluck(:data)).to match_array(expected_rows) expect(account.bulk_imports.first.rows.pluck(:data)).to match_array(expected_rows)
end end
it 'creates a BulkImport with expected attributes' do context 'with a BulkImport' do
bulk_import = account.bulk_imports.first let(:bulk_import) { account.bulk_imports.first }
it 'creates a non-nil bulk import' do
expect(bulk_import).to_not be_nil expect(bulk_import).to_not be_nil
end
it 'matches the subjects type' do
expect(bulk_import.type.to_sym).to eq subject.type.to_sym expect(bulk_import.type.to_sym).to eq subject.type.to_sym
end
it 'matches the subjects original filename' do
expect(bulk_import.original_filename).to eq subject.data.original_filename expect(bulk_import.original_filename).to eq subject.data.original_filename
end
it 'matches the subjects likely_mismatched? value' do
expect(bulk_import.likely_mismatched?).to eq subject.likely_mismatched? expect(bulk_import.likely_mismatched?).to eq subject.likely_mismatched?
end
it 'matches the subject overwrite value' do
expect(bulk_import.overwrite?).to eq !!subject.overwrite # rubocop:disable Style/DoubleNegation expect(bulk_import.overwrite?).to eq !!subject.overwrite # rubocop:disable Style/DoubleNegation
end
it 'has zero processed items' do
expect(bulk_import.processed_items).to eq 0 expect(bulk_import.processed_items).to eq 0
end
it 'has zero imported items' do
expect(bulk_import.imported_items).to eq 0 expect(bulk_import.imported_items).to eq 0
end
it 'has a correct total_items value' do
expect(bulk_import.total_items).to eq bulk_import.rows.count expect(bulk_import.total_items).to eq bulk_import.rows.count
end
it 'defaults to unconfirmed true' do
expect(bulk_import.unconfirmed?).to be true expect(bulk_import.unconfirmed?).to be true
end end
end end
end
it_behaves_like 'on successful import', 'following', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) it_behaves_like 'on successful import', 'following', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
it_behaves_like 'on successful import', 'following', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) it_behaves_like 'on successful import', 'following', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })

View File

@ -99,70 +99,83 @@ RSpec.describe Notification do
] ]
end end
it 'preloads target status' do context 'with a preloaded target status' do
# mention it 'preloads mention' do
expect(subject[0].type).to eq :mention expect(subject[0].type).to eq :mention
expect(subject[0].association(:mention)).to be_loaded expect(subject[0].association(:mention)).to be_loaded
expect(subject[0].mention.association(:status)).to be_loaded expect(subject[0].mention.association(:status)).to be_loaded
end
# status it 'preloads status' do
expect(subject[1].type).to eq :status expect(subject[1].type).to eq :status
expect(subject[1].association(:status)).to be_loaded expect(subject[1].association(:status)).to be_loaded
end
# reblog it 'preloads reblog' do
expect(subject[2].type).to eq :reblog expect(subject[2].type).to eq :reblog
expect(subject[2].association(:status)).to be_loaded expect(subject[2].association(:status)).to be_loaded
expect(subject[2].status.association(:reblog)).to be_loaded expect(subject[2].status.association(:reblog)).to be_loaded
end
# follow: nothing it 'preloads follow as nil' do
expect(subject[3].type).to eq :follow expect(subject[3].type).to eq :follow
expect(subject[3].target_status).to be_nil expect(subject[3].target_status).to be_nil
end
# follow_request: nothing it 'preloads follow_request as nill' do
expect(subject[4].type).to eq :follow_request expect(subject[4].type).to eq :follow_request
expect(subject[4].target_status).to be_nil expect(subject[4].target_status).to be_nil
end
# favourite it 'preloads favourite' do
expect(subject[5].type).to eq :favourite expect(subject[5].type).to eq :favourite
expect(subject[5].association(:favourite)).to be_loaded expect(subject[5].association(:favourite)).to be_loaded
expect(subject[5].favourite.association(:status)).to be_loaded expect(subject[5].favourite.association(:status)).to be_loaded
end
# poll it 'preloads poll' do
expect(subject[6].type).to eq :poll expect(subject[6].type).to eq :poll
expect(subject[6].association(:poll)).to be_loaded expect(subject[6].association(:poll)).to be_loaded
expect(subject[6].poll.association(:status)).to be_loaded expect(subject[6].poll.association(:status)).to be_loaded
end end
end
it 'replaces to cached status' do context 'with a cached status' do
# mention it 'replaces mention' do
expect(subject[0].type).to eq :mention expect(subject[0].type).to eq :mention
expect(subject[0].target_status.association(:account)).to be_loaded expect(subject[0].target_status.association(:account)).to be_loaded
expect(subject[0].target_status).to eq mention.status expect(subject[0].target_status).to eq mention.status
end
# status it 'replaces status' do
expect(subject[1].type).to eq :status expect(subject[1].type).to eq :status
expect(subject[1].target_status.association(:account)).to be_loaded expect(subject[1].target_status.association(:account)).to be_loaded
expect(subject[1].target_status).to eq status expect(subject[1].target_status).to eq status
end
# reblog it 'replaces reblog' do
expect(subject[2].type).to eq :reblog expect(subject[2].type).to eq :reblog
expect(subject[2].target_status.association(:account)).to be_loaded expect(subject[2].target_status.association(:account)).to be_loaded
expect(subject[2].target_status).to eq reblog.reblog expect(subject[2].target_status).to eq reblog.reblog
end
# follow: nothing it 'replaces follow' do
expect(subject[3].type).to eq :follow expect(subject[3].type).to eq :follow
expect(subject[3].target_status).to be_nil expect(subject[3].target_status).to be_nil
end
# follow_request: nothing it 'replaces follow_request' do
expect(subject[4].type).to eq :follow_request expect(subject[4].type).to eq :follow_request
expect(subject[4].target_status).to be_nil expect(subject[4].target_status).to be_nil
end
# favourite it 'replaces favourite' do
expect(subject[5].type).to eq :favourite expect(subject[5].type).to eq :favourite
expect(subject[5].target_status.association(:account)).to be_loaded expect(subject[5].target_status.association(:account)).to be_loaded
expect(subject[5].target_status).to eq favourite.status expect(subject[5].target_status).to eq favourite.status
end
# poll it 'replaces poll' do
expect(subject[6].type).to eq :poll expect(subject[6].type).to eq :poll
expect(subject[6].target_status.association(:account)).to be_loaded expect(subject[6].target_status.association(:account)).to be_loaded
expect(subject[6].target_status).to eq poll.status expect(subject[6].target_status).to eq poll.status
@ -170,3 +183,4 @@ RSpec.describe Notification do
end end
end end
end end
end

View File

@ -79,6 +79,7 @@ RSpec.configure do |config|
config.before :each, type: :cli do config.before :each, type: :cli do
stub_stdout stub_stdout
stub_reset_connection_pools
end end
config.before :each, type: :feature do config.before :each, type: :feature do
@ -121,9 +122,20 @@ def attachment_fixture(name)
end end
def stub_stdout def stub_stdout
# TODO: Is there a bettery way to:
# - Avoid CLI command output being printed out
# - Allow rspec to assert things against STDOUT
# - Avoid disabling stdout for other desirable output (deprecation warnings, for example)
allow($stdout).to receive(:write) allow($stdout).to receive(:write)
end end
def stub_reset_connection_pools
# TODO: Is there a better way to correctly run specs without stubbing this?
# (Avoids reset_connection_pools! in test env)
allow(ActiveRecord::Base).to receive(:establish_connection)
allow(RedisConfiguration).to receive(:establish_pool)
end
def stub_jsonld_contexts! def stub_jsonld_contexts!
stub_request(:get, 'https://www.w3.org/ns/activitystreams').to_return(request_fixture('json-ld.activitystreams.txt')) stub_request(:get, 'https://www.w3.org/ns/activitystreams').to_return(request_fixture('json-ld.activitystreams.txt'))
stub_request(:get, 'https://w3id.org/identity/v1').to_return(request_fixture('json-ld.identity.txt')) stub_request(:get, 'https://w3id.org/identity/v1').to_return(request_fixture('json-ld.identity.txt'))

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'GET /api/v1/accounts/{account_id}' do
it 'returns account entity as 200 OK' do
account = Fabricate(:account)
get "/api/v1/accounts/#{account.id}"
aggregate_failures do
expect(response).to have_http_status(200)
expect(body_as_json[:id]).to eq(account.id.to_s)
end
end
it 'returns 404 if account not found' do
get '/api/v1/accounts/1'
aggregate_failures do
expect(response).to have_http_status(404)
expect(body_as_json[:error]).to eq('Record not found')
end
end
context 'when with token' do
it 'returns account entity as 200 OK if token is valid' do
account = Fabricate(:account)
user = Fabricate(:user, account: account)
token = Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts').token
get "/api/v1/accounts/#{account.id}", headers: { Authorization: "Bearer #{token}" }
aggregate_failures do
expect(response).to have_http_status(200)
expect(body_as_json[:id]).to eq(account.id.to_s)
end
end
it 'returns 403 if scope of token is invalid' do
account = Fabricate(:account)
user = Fabricate(:user, account: account)
token = Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:statuses').token
get "/api/v1/accounts/#{account.id}", headers: { Authorization: "Bearer #{token}" }
aggregate_failures do
expect(response).to have_http_status(403)
expect(body_as_json[:error]).to eq('This action is outside the authorized scopes')
end
end
end
end

View File

@ -0,0 +1,305 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Canonical Email Blocks' do
let(:role) { UserRole.find_by(name: 'Admin') }
let(:user) { Fabricate(:user, role: role) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:scopes) { 'admin:read:canonical_email_blocks admin:write:canonical_email_blocks' }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
subject
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
subject
expect(response).to have_http_status(403)
end
end
describe 'GET /api/v1/admin/canonical_email_blocks' do
subject do
get '/api/v1/admin/canonical_email_blocks', headers: headers, params: params
end
let(:params) { {} }
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
context 'when there is no canonical email block' do
it 'returns an empty list' do
subject
expect(body_as_json).to be_empty
end
end
context 'when there are canonical email blocks' do
let!(:canonical_email_blocks) { Fabricate.times(5, :canonical_email_block) }
let(:expected_email_hashes) { canonical_email_blocks.pluck(:canonical_email_hash) }
it 'returns the correct canonical email hashes' do
subject
expect(body_as_json.pluck(:canonical_email_hash)).to match_array(expected_email_hashes)
end
context 'with limit param' do
let(:params) { { limit: 2 } }
it 'returns only the requested number of canonical email blocks' do
subject
expect(body_as_json.size).to eq(params[:limit])
end
end
context 'with since_id param' do
let(:params) { { since_id: canonical_email_blocks[1].id } }
it 'returns only the canonical email blocks after since_id' do
subject
canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s)
expect(body_as_json.pluck(:id)).to match_array(canonical_email_blocks_ids[2..])
end
end
context 'with max_id param' do
let(:params) { { max_id: canonical_email_blocks[3].id } }
it 'returns only the canonical email blocks before max_id' do
subject
canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s)
expect(body_as_json.pluck(:id)).to match_array(canonical_email_blocks_ids[..2])
end
end
end
end
describe 'GET /api/v1/admin/canonical_email_blocks/:id' do
subject do
get "/api/v1/admin/canonical_email_blocks/#{canonical_email_block.id}", headers: headers
end
let!(:canonical_email_block) { Fabricate(:canonical_email_block) }
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
context 'when the requested canonical email block exists' do
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the requested canonical email block data correctly' do
subject
json = body_as_json
expect(json[:id]).to eq(canonical_email_block.id.to_s)
expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
end
end
context 'when the requested canonical block does not exist' do
it 'returns http not found' do
get '/api/v1/admin/canonical_email_blocks/-1', headers: headers
expect(response).to have_http_status(404)
end
end
end
describe 'POST /api/v1/admin/canonical_email_blocks/test' do
subject do
post '/api/v1/admin/canonical_email_blocks/test', headers: headers, params: params
end
let(:params) { { email: 'email@example.com' } }
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
context 'when the required email param is not provided' do
let(:params) { {} }
it 'returns http bad request' do
subject
expect(response).to have_http_status(400)
end
end
context 'when the required email param is provided' do
context 'when there is a matching canonical email block' do
let!(:canonical_email_block) { CanonicalEmailBlock.create(params) }
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the expected canonical email hash' do
subject
expect(body_as_json[0][:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
end
end
context 'when there is no matching canonical email block' do
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns an empty list' do
subject
expect(body_as_json).to be_empty
end
end
end
end
describe 'POST /api/v1/admin/canonical_email_blocks' do
subject do
post '/api/v1/admin/canonical_email_blocks', headers: headers, params: params
end
let(:params) { { email: 'example@email.com' } }
let(:canonical_email_block) { CanonicalEmailBlock.new(email: params[:email]) }
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the canonical_email_hash correctly' do
subject
expect(body_as_json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
end
context 'when the required email param is not provided' do
let(:params) { {} }
it 'returns http unprocessable entity' do
subject
expect(response).to have_http_status(422)
end
end
context 'when the canonical_email_hash param is provided instead of email' do
let(:params) { { canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } }
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the correct canonical_email_hash' do
subject
expect(body_as_json[:canonical_email_hash]).to eq(params[:canonical_email_hash])
end
end
context 'when both email and canonical_email_hash params are provided' do
let(:params) { { email: 'example@email.com', canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } }
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'ignores the canonical_email_hash param' do
subject
expect(body_as_json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
end
end
context 'when the given canonical email was already blocked' do
before do
canonical_email_block.save
end
it 'returns http unprocessable entity' do
subject
expect(response).to have_http_status(422)
end
end
end
describe 'DELETE /api/v1/admin/canonical_email_blocks/:id' do
subject do
delete "/api/v1/admin/canonical_email_blocks/#{canonical_email_block.id}", headers: headers
end
let!(:canonical_email_block) { Fabricate(:canonical_email_block) }
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'deletes the canonical email block' do
subject
expect(CanonicalEmailBlock.find_by(id: canonical_email_block.id)).to be_nil
end
context 'when the canonical email block is not found' do
it 'returns http not found' do
delete '/api/v1/admin/canonical_email_blocks/0', headers: headers
expect(response).to have_http_status(404)
end
end
end
end

View File

@ -0,0 +1,214 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Domain Allows' do
let(:role) { UserRole.find_by(name: 'Admin') }
let(:user) { Fabricate(:user, role: role) }
let(:scopes) { 'admin:read admin:write' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
subject
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
subject
expect(response).to have_http_status(403)
end
end
describe 'GET /api/v1/admin/domain_allows' do
subject do
get '/api/v1/admin/domain_allows', headers: headers, params: params
end
let(:params) { {} }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
context 'when there is no allowed domains' do
it 'returns an empty body' do
subject
expect(body_as_json).to be_empty
end
end
context 'when there are allowed domains' do
let!(:domain_allows) { Fabricate.times(5, :domain_allow) }
let(:expected_response) do
domain_allows.map do |domain_allow|
{
id: domain_allow.id.to_s,
domain: domain_allow.domain,
created_at: domain_allow.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
}
end
end
it 'returns the correct allowed domains' do
subject
expect(body_as_json).to match_array(expected_response)
end
context 'with limit param' do
let(:params) { { limit: 2 } }
it 'returns only the requested number of allowed domains' do
subject
expect(body_as_json.size).to eq(params[:limit])
end
end
end
end
describe 'GET /api/v1/admin/domain_allows/:id' do
subject do
get "/api/v1/admin/domain_allows/#{domain_allow.id}", headers: headers
end
let!(:domain_allow) { Fabricate(:domain_allow) }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the expected allowed domain name' do
subject
expect(body_as_json[:domain]).to eq domain_allow.domain
end
context 'when the requested allowed domain does not exist' do
it 'returns http not found' do
get '/api/v1/admin/domain_allows/-1', headers: headers
expect(response).to have_http_status(404)
end
end
end
describe 'POST /api/v1/admin/domain_allows' do
subject do
post '/api/v1/admin/domain_allows', headers: headers, params: params
end
let(:params) { { domain: 'foo.bar.com' } }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
context 'with a valid domain name' do
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the expected domain name' do
subject
expect(body_as_json[:domain]).to eq 'foo.bar.com'
end
it 'creates a domain allow' do
subject
expect(DomainAllow.find_by(domain: 'foo.bar.com')).to be_present
end
end
context 'with invalid domain name' do
let(:params) { 'foo bar' }
it 'returns http unprocessable entity' do
subject
expect(response).to have_http_status(422)
end
end
context 'when domain name is not specified' do
let(:params) { {} }
it 'returns http unprocessable entity' do
subject
expect(response).to have_http_status(422)
end
end
context 'when the domain is already allowed' do
before do
DomainAllow.create(params)
end
it 'returns the existing allowed domain name' do
subject
expect(body_as_json[:domain]).to eq(params[:domain])
end
end
end
describe 'DELETE /api/v1/admin/domain_allows/:id' do
subject do
delete "/api/v1/admin/domain_allows/#{domain_allow.id}", headers: headers
end
let!(:domain_allow) { Fabricate(:domain_allow) }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'deletes the allowed domain' do
subject
expect(DomainAllow.find_by(id: domain_allow.id)).to be_nil
end
context 'when the allowed domain does not exist' do
it 'returns http not found' do
delete '/api/v1/admin/domain_allows/-1', headers: headers
expect(response).to have_http_status(404)
end
end
end
end

View File

@ -0,0 +1,284 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Domain Blocks' do
let(:role) { UserRole.find_by(name: 'Admin') }
let(:user) { Fabricate(:user, role: role) }
let(:scopes) { 'admin:read:domain_blocks admin:write:domain_blocks' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
subject
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
subject
expect(response).to have_http_status(403)
end
end
describe 'GET /api/v1/admin/domain_blocks' do
subject do
get '/api/v1/admin/domain_blocks', headers: headers, params: params
end
let(:params) { {} }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
context 'when there are no domain blocks' do
it 'returns an empty list' do
subject
expect(body_as_json).to be_empty
end
end
context 'when there are domain blocks' do
let!(:domain_blocks) do
[
Fabricate(:domain_block, severity: :silence, reject_media: true),
Fabricate(:domain_block, severity: :suspend, obfuscate: true),
Fabricate(:domain_block, severity: :noop, reject_reports: true),
Fabricate(:domain_block, public_comment: 'Spam'),
Fabricate(:domain_block, private_comment: 'Spam'),
]
end
let(:expected_responde) do
domain_blocks.map do |domain_block|
{
id: domain_block.id.to_s,
domain: domain_block.domain,
created_at: domain_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
severity: domain_block.severity.to_s,
reject_media: domain_block.reject_media,
reject_reports: domain_block.reject_reports,
private_comment: domain_block.private_comment,
public_comment: domain_block.public_comment,
obfuscate: domain_block.obfuscate,
}
end
end
it 'returns the expected domain blocks' do
subject
expect(body_as_json).to match_array(expected_responde)
end
context 'with limit param' do
let(:params) { { limit: 2 } }
it 'returns only the requested number of domain blocks' do
subject
expect(body_as_json.size).to eq(params[:limit])
end
end
end
end
describe 'GET /api/v1/admin/domain_blocks/:id' do
subject do
get "/api/v1/admin/domain_blocks/#{domain_block.id}", headers: headers
end
let!(:domain_block) { Fabricate(:domain_block) }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the expected domain block content' do
subject
expect(body_as_json).to eq(
{
id: domain_block.id.to_s,
domain: domain_block.domain,
created_at: domain_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
severity: domain_block.severity.to_s,
reject_media: domain_block.reject_media,
reject_reports: domain_block.reject_reports,
private_comment: domain_block.private_comment,
public_comment: domain_block.public_comment,
obfuscate: domain_block.obfuscate,
}
)
end
context 'when the requested domain block does not exist' do
it 'returns http not found' do
get '/api/v1/admin/domain_blocks/-1', headers: headers
expect(response).to have_http_status(404)
end
end
end
describe 'POST /api/v1/admin/domain_blocks' do
subject do
post '/api/v1/admin/domain_blocks', headers: headers, params: params
end
let(:params) { { domain: 'foo.bar.com', severity: :silence } }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns expected domain name and severity' do
subject
body = body_as_json
expect(body).to match a_hash_including(
{
domain: 'foo.bar.com',
severity: 'silence',
}
)
end
it 'creates a domain block' do
subject
expect(DomainBlock.find_by(domain: 'foo.bar.com')).to be_present
end
context 'when a stricter domain block already exists' do
before do
Fabricate(:domain_block, domain: 'bar.com', severity: :suspend)
end
it 'returns http unprocessable entity' do
subject
expect(response).to have_http_status(422)
end
it 'returns existing domain block in error' do
subject
expect(body_as_json[:existing_domain_block][:domain]).to eq('bar.com')
end
end
context 'when given domain name is invalid' do
let(:params) { { domain: 'foo bar', severity: :silence } }
it 'returns http unprocessable entity' do
subject
expect(response).to have_http_status(422)
end
end
end
describe 'PUT /api/v1/admin/domain_blocks/:id' do
subject do
put "/api/v1/admin/domain_blocks/#{domain_block.id}", headers: headers, params: params
end
let!(:domain_block) { Fabricate(:domain_block, domain: 'example.com', severity: :silence) }
let(:params) { { domain: 'example.com', severity: 'suspend' } }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the updated domain block' do
subject
expect(body_as_json).to match a_hash_including(
{
id: domain_block.id.to_s,
domain: domain_block.domain,
severity: 'suspend',
}
)
end
it 'updates the block severity' do
expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend')
end
context 'when domain block does not exist' do
it 'returns http not found' do
put '/api/v1/admin/domain_blocks/-1', headers: headers
expect(response).to have_http_status(404)
end
end
end
describe 'DELETE /api/v1/admin/domain_blocks/:id' do
subject do
delete "/api/v1/admin/domain_blocks/#{domain_block.id}", headers: headers
end
let!(:domain_block) { Fabricate(:domain_block) }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'deletes the domain block' do
subject
expect(DomainBlock.find_by(id: domain_block.id)).to be_nil
end
context 'when domain block does not exist' do
it 'returns http not found' do
delete '/api/v1/admin/domain_blocks/-1', headers: headers
expect(response).to have_http_status(404)
end
end
end
end

View File

@ -0,0 +1,275 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'IP Blocks' do
let(:role) { UserRole.find_by(name: 'Admin') }
let(:user) { Fabricate(:user, role: role) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:scopes) { 'admin:read:ip_blocks admin:write:ip_blocks' }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
subject
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
subject
expect(response).to have_http_status(403)
end
end
describe 'GET /api/v1/admin/ip_blocks' do
subject do
get '/api/v1/admin/ip_blocks', headers: headers, params: params
end
let(:params) { {} }
it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
context 'when there is no ip block' do
it 'returns an empty body' do
subject
expect(body_as_json).to be_empty
end
end
context 'when there are ip blocks' do
let!(:ip_blocks) do
[
IpBlock.create(ip: '192.0.2.0/24', severity: :no_access),
IpBlock.create(ip: '172.16.0.1', severity: :sign_up_requires_approval, comment: 'Spam'),
IpBlock.create(ip: '2001:0db8::/32', severity: :sign_up_block, expires_in: 10.days),
]
end
let(:expected_response) do
ip_blocks.map do |ip_block|
{
id: ip_block.id.to_s,
ip: ip_block.ip,
severity: ip_block.severity.to_s,
comment: ip_block.comment,
created_at: ip_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
expires_at: ip_block.expires_at&.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
}
end
end
it 'returns the correct blocked ips' do
subject
expect(body_as_json).to match_array(expected_response)
end
context 'with limit param' do
let(:params) { { limit: 2 } }
it 'returns only the requested number of ip blocks' do
subject
expect(body_as_json.size).to eq(params[:limit])
end
end
end
end
describe 'GET /api/v1/admin/ip_blocks/:id' do
subject do
get "/api/v1/admin/ip_blocks/#{ip_block.id}", headers: headers
end
let!(:ip_block) { IpBlock.create(ip: '192.0.2.0/24', severity: :no_access) }
it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the correct ip block' do
subject
json = body_as_json
expect(json[:ip]).to eq("#{ip_block.ip}/#{ip_block.ip.prefix}")
expect(json[:severity]).to eq(ip_block.severity.to_s)
end
context 'when ip block does not exist' do
it 'returns http not found' do
get '/api/v1/admin/ip_blocks/-1', headers: headers
expect(response).to have_http_status(404)
end
end
end
describe 'POST /api/v1/admin/ip_blocks' do
subject do
post '/api/v1/admin/ip_blocks', headers: headers, params: params
end
let(:params) { { ip: '151.0.32.55', severity: 'no_access', comment: 'Spam' } }
it_behaves_like 'forbidden for wrong scope', 'admin:read:ip_blocks'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the correct ip block' do
subject
json = body_as_json
expect(json[:ip]).to eq("#{params[:ip]}/32")
expect(json[:severity]).to eq(params[:severity])
expect(json[:comment]).to eq(params[:comment])
end
context 'when the required ip param is not provided' do
let(:params) { { ip: '', severity: 'no_access' } }
it 'returns http unprocessable entity' do
subject
expect(response).to have_http_status(422)
end
end
context 'when the required severity param is not provided' do
let(:params) { { ip: '173.65.23.1', severity: '' } }
it 'returns http unprocessable entity' do
subject
expect(response).to have_http_status(422)
end
end
context 'when the given ip address is already blocked' do
before do
IpBlock.create(params)
end
it 'returns http unprocessable entity' do
subject
expect(response).to have_http_status(422)
end
end
context 'when the given ip address is invalid' do
let(:params) { { ip: '520.13.54.120', severity: 'no_access' } }
it 'returns http unprocessable entity' do
subject
expect(response).to have_http_status(422)
end
end
end
describe 'PUT /api/v1/admin/ip_blocks/:id' do
subject do
put "/api/v1/admin/ip_blocks/#{ip_block.id}", headers: headers, params: params
end
let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access', comment: 'Spam', expires_in: 48.hours) }
let(:params) { { severity: 'sign_up_requires_approval', comment: 'Decreasing severity' } }
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the correct ip block' do
subject
expect(body_as_json).to match(hash_including({
ip: "#{ip_block.ip}/#{ip_block.ip.prefix}",
severity: 'sign_up_requires_approval',
comment: 'Decreasing severity',
}))
end
it 'updates the severity correctly' do
expect { subject }.to change { ip_block.reload.severity }.from('no_access').to('sign_up_requires_approval')
end
it 'updates the comment correctly' do
expect { subject }.to change { ip_block.reload.comment }.from('Spam').to('Decreasing severity')
end
context 'when ip block does not exist' do
it 'returns http not found' do
put '/api/v1/admin/ip_blocks/-1', headers: headers, params: params
expect(response).to have_http_status(404)
end
end
end
describe 'DELETE /api/v1/admin/ip_blocks/:id' do
subject do
delete "/api/v1/admin/ip_blocks/#{ip_block.id}", headers: headers
end
let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access') }
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns an empty body' do
subject
expect(body_as_json).to be_empty
end
it 'deletes the ip block' do
subject
expect(IpBlock.find_by(id: ip_block.id)).to be_nil
end
context 'when ip block does not exist' do
it 'returns http not found' do
delete '/api/v1/admin/ip_blocks/-1', headers: headers
expect(response).to have_http_status(404)
end
end
end
end

View File

@ -0,0 +1,292 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Reports' do
let(:role) { UserRole.find_by(name: 'Admin') }
let(:user) { Fabricate(:user, role: role) }
let(:scopes) { 'admin:read:reports admin:write:reports' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
subject
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
subject
expect(response).to have_http_status(403)
end
end
describe 'GET /api/v1/admin/reports' do
subject do
get '/api/v1/admin/reports', headers: headers, params: params
end
let(:params) { {} }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
context 'when there are no reports' do
it 'returns an empty list' do
subject
expect(body_as_json).to be_empty
end
end
context 'when there are reports' do
let!(:reporter) { Fabricate(:account) }
let!(:spammer) { Fabricate(:account) }
let(:expected_response) do
scope.map do |report|
hash_including({
id: report.id.to_s,
action_taken: report.action_taken?,
category: report.category,
comment: report.comment,
account: hash_including(id: report.account.id.to_s),
target_account: hash_including(id: report.target_account.id.to_s),
statuses: report.statuses,
rules: report.rules,
forwarded: report.forwarded,
})
end
end
let(:scope) { Report.unresolved }
before do
Fabricate(:report)
Fabricate(:report, target_account: spammer)
Fabricate(:report, account: reporter, target_account: spammer)
Fabricate(:report, action_taken_at: 4.days.ago, account: reporter)
Fabricate(:report, action_taken_at: 20.days.ago)
end
it 'returns all unresolved reports' do
subject
expect(body_as_json).to match_array(expected_response)
end
context 'with resolved param' do
let(:params) { { resolved: true } }
let(:scope) { Report.resolved }
it 'returns only the resolved reports' do
subject
expect(body_as_json).to match_array(expected_response)
end
end
context 'with account_id param' do
let(:params) { { account_id: reporter.id } }
let(:scope) { Report.unresolved.where(account: reporter) }
it 'returns all unresolved reports filed by the specified account' do
subject
expect(body_as_json).to match_array(expected_response)
end
end
context 'with target_account_id param' do
let(:params) { { target_account_id: spammer.id } }
let(:scope) { Report.unresolved.where(target_account: spammer) }
it 'returns all unresolved reports targeting the specified account' do
subject
expect(body_as_json).to match_array(expected_response)
end
end
context 'with limit param' do
let(:params) { { limit: 1 } }
it 'returns only the requested number of reports' do
subject
expect(body_as_json.size).to eq(1)
end
end
end
end
describe 'GET /api/v1/admin/reports/:id' do
subject do
get "/api/v1/admin/reports/#{report.id}", headers: headers
end
let(:report) { Fabricate(:report) }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the requested report content' do
subject
expect(body_as_json).to include(
{
id: report.id.to_s,
action_taken: report.action_taken?,
category: report.category,
comment: report.comment,
account: a_hash_including(id: report.account.id.to_s),
target_account: a_hash_including(id: report.target_account.id.to_s),
statuses: report.statuses,
rules: report.rules,
forwarded: report.forwarded,
}
)
end
end
describe 'PUT /api/v1/admin/reports/:id' do
subject do
put "/api/v1/admin/reports/#{report.id}", headers: headers, params: params
end
let!(:report) { Fabricate(:report, category: :other) }
let(:params) { { category: 'spam' } }
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'updates the report category' do
expect { subject }.to change { report.reload.category }.from('other').to('spam')
end
it 'returns the updated report content' do
subject
report.reload
expect(body_as_json).to include(
{
id: report.id.to_s,
action_taken: report.action_taken?,
category: report.category,
comment: report.comment,
account: a_hash_including(id: report.account.id.to_s),
target_account: a_hash_including(id: report.target_account.id.to_s),
statuses: report.statuses,
rules: report.rules,
forwarded: report.forwarded,
}
)
end
end
describe 'POST #resolve' do
subject do
post "/api/v1/admin/reports/#{report.id}/resolve", headers: headers
end
let(:report) { Fabricate(:report, action_taken_at: nil) }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'marks report as resolved' do
expect { subject }.to change { report.reload.unresolved? }.from(true).to(false)
end
end
describe 'POST #reopen' do
subject do
post "/api/v1/admin/reports/#{report.id}/reopen", headers: headers
end
let(:report) { Fabricate(:report, action_taken_at: 10.days.ago) }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'marks report as unresolved' do
expect { subject }.to change { report.reload.unresolved? }.from(false).to(true)
end
end
describe 'POST #assign_to_self' do
subject do
post "/api/v1/admin/reports/#{report.id}/assign_to_self", headers: headers
end
let(:report) { Fabricate(:report) }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'assigns report to the requesting user' do
expect { subject }.to change { report.reload.assigned_account_id }.from(nil).to(user.account.id)
end
end
describe 'POST #unassign' do
subject do
post "/api/v1/admin/reports/#{report.id}/unassign", headers: headers
end
let(:report) { Fabricate(:report, assigned_account_id: user.account.id) }
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'unassigns report from assignee' do
expect { subject }.to change { report.reload.assigned_account_id }.from(user.account.id).to(nil)
end
end
end

View File

@ -152,24 +152,33 @@ RSpec.describe TranslateStatusService, type: :service do
describe 'status has poll' do describe 'status has poll' do
let(:poll) { Fabricate(:poll, options: %w(Blue Green)) } let(:poll) { Fabricate(:poll, options: %w(Blue Green)) }
context 'with source texts from the service' do
let!(:source_texts) { service.send(:source_texts) }
it 'returns formatted poll options' do it 'returns formatted poll options' do
source_texts = service.send(:source_texts)
expect(source_texts.size).to eq 3 expect(source_texts.size).to eq 3
expect(source_texts.values).to eq %w(<p>Hello</p> Blue Green) expect(source_texts.values).to eq %w(<p>Hello</p> Blue Green)
end
it 'has a first key with content' do
expect(source_texts.keys.first).to eq :content expect(source_texts.keys.first).to eq :content
end
it 'has the first option in the second key with correct options' do
option1 = source_texts.keys.second option1 = source_texts.keys.second
expect(option1).to be_a Poll::Option expect(option1).to be_a Poll::Option
expect(option1.id).to eq '0' expect(option1.id).to eq '0'
expect(option1.title).to eq 'Blue' expect(option1.title).to eq 'Blue'
end
it 'has the second option in the third key with correct options' do
option2 = source_texts.keys.third option2 = source_texts.keys.third
expect(option2).to be_a Poll::Option expect(option2).to be_a Poll::Option
expect(option2.id).to eq '1' expect(option2.id).to eq '1'
expect(option2.title).to eq 'Green' expect(option2.title).to eq 'Green'
end end
end end
end
describe 'status has poll with custom emoji' do describe 'status has poll with custom emoji' do
let(:poll) { Fabricate(:poll, options: ['Blue', 'Green :highfive:']) } let(:poll) { Fabricate(:poll, options: ['Blue', 'Green :highfive:']) }

View File

@ -1,74 +0,0 @@
# frozen_string_literal: true
shared_examples 'ScopedSettings' do
describe '[]' do
it 'inherits default settings' do
expect(Setting.boost_modal).to be false
expect(Setting.interactions['must_be_follower']).to be false
settings = create!
expect(settings['boost_modal']).to be false
expect(settings['interactions']['must_be_follower']).to be false
end
end
describe 'all_as_records' do
# expecting [] and []= works
it 'returns records merged with default values except hashes' do
expect(Setting.boost_modal).to be false
expect(Setting.delete_modal).to be true
settings = create!
settings['boost_modal'] = true
records = settings.all_as_records
expect(records['boost_modal'].value).to be true
expect(records['delete_modal'].value).to be true
end
end
describe 'missing methods' do
# expecting [] and []= works.
it 'reads settings' do
expect(Setting.boost_modal).to be false
settings = create!
expect(settings.boost_modal).to be false
end
it 'updates settings' do
settings = fabricate
settings.boost_modal = true
expect(settings['boost_modal']).to be true
end
end
it 'can update settings with [] and can read with []=' do
settings = fabricate
settings['boost_modal'] = true
settings['interactions'] = settings['interactions'].merge('must_be_follower' => true)
Setting.save!
expect(settings['boost_modal']).to be true
expect(settings['interactions']['must_be_follower']).to be true
Rails.cache.clear
expect(settings['boost_modal']).to be true
expect(settings['interactions']['must_be_follower']).to be true
end
xit 'does not mutate defaults via the cache' do
fabricate['interactions']['must_be_follower'] = true
# TODO
# This mutates the global settings default such that future
# instances will inherit the incorrect starting values
expect(fabricate.settings['interactions']['must_be_follower']).to be false
end
end

View File

@ -1,15 +0,0 @@
# frozen_string_literal: true
shared_examples 'Settings-extended' do
describe 'settings' do
def fabricate
super.settings
end
def create!
super.settings
end
it_behaves_like 'ScopedSettings'
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
shared_examples 'AccountAvatar' do |fabricator| shared_examples 'AccountAvatar' do |fabricator|
describe 'static avatars' do describe 'static avatars', paperclip_processing: true do
describe 'when GIF' do describe 'when GIF' do
it 'creates a png static style' do it 'creates a png static style' do
account = Fabricate(fabricator, avatar: attachment_fixture('avatar.gif')) account = Fabricate(fabricator, avatar: attachment_fixture('avatar.gif'))

View File

@ -835,7 +835,11 @@ const startServer = async () => {
return; return;
} }
ws.send(JSON.stringify({ stream: streamName, event, payload })); ws.send(JSON.stringify({ stream: streamName, event, payload }), (err) => {
if (err) {
log.error(req.requestId, `Failed to send to websocket: ${err}`);
}
});
}; };
/** /**