diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 69dd433e3..77caad492 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -239,31 +239,6 @@ Naming/VariableNumber: - 'spec/models/user_spec.rb' - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/MapCompact: - Exclude: - - 'app/lib/admin/metrics/dimension.rb' - - 'app/lib/admin/metrics/measure.rb' - - 'app/lib/feed_manager.rb' - - 'app/models/account.rb' - - 'app/models/account_statuses_cleanup_policy.rb' - - 'app/models/account_suggestions/setting_source.rb' - - 'app/models/account_suggestions/source.rb' - - 'app/models/follow_recommendation_filter.rb' - - 'app/models/notification.rb' - - 'app/models/user_role.rb' - - 'app/models/webhook.rb' - - 'app/services/process_mentions_service.rb' - - 'app/validators/existing_username_validator.rb' - - 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb' - - 'spec/presenters/status_relationships_presenter_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SafeMultiline. -Performance/StartWith: - Exclude: - - 'app/lib/extractor.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Performance/UnfreezeString: Exclude: @@ -599,10 +574,6 @@ RSpec/PredicateMatcher: - 'spec/models/user_spec.rb' - 'spec/services/post_status_service_spec.rb' -RSpec/RepeatedExample: - Exclude: - - 'spec/policies/status_policy_spec.rb' - RSpec/StubbedMock: Exclude: - 'spec/controllers/api/base_controller_spec.rb' diff --git a/Gemfile b/Gemfile index 67a0fb078..88ac7c5ae 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ gem 'makara', '~> 0.5' gem 'pghero' gem 'dotenv-rails', '~> 2.8' -gem 'aws-sdk-s3', '~> 1.120', require: false +gem 'aws-sdk-s3', '~> 1.122', require: false gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 0.3', require: false gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b' @@ -75,7 +75,7 @@ gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-s gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' -gem 'rqrcode', '~> 2.1' +gem 'rqrcode', '~> 2.2' gem 'ruby-progressbar', '~> 1.13' gem 'sanitize', '~> 6.0' gem 'scenic', '~> 1.7' diff --git a/Gemfile.lock b/Gemfile.lock index acea3bbbe..21647e1b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,16 +109,16 @@ GEM attr_required (1.0.1) awrence (1.2.1) aws-eventstream (1.2.0) - aws-partitions (1.752.0) - aws-sdk-core (3.171.0) + aws-partitions (1.761.0) + aws-sdk-core (3.172.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.63.0) + aws-sdk-kms (1.64.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.121.0) + aws-sdk-s3 (1.122.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) @@ -189,7 +189,7 @@ GEM coderay (1.1.3) color_diff (0.1) concurrent-ruby (1.2.2) - connection_pool (2.4.0) + connection_pool (2.4.1) cose (1.3.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) @@ -398,9 +398,9 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.20.0) + loofah (2.21.3) crass (~> 1.0.2) - nokogiri (>= 1.5.9) + nokogiri (>= 1.12.0) mail (2.8.1) mini_mime (>= 0.1.1) net-imap @@ -576,7 +576,7 @@ GEM rexml (3.2.5) rotp (6.2.2) rpam2 (4.0.2) - rqrcode (2.1.2) + rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) @@ -588,20 +588,20 @@ GEM rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.0.1) + rspec-rails (6.0.2) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) - rspec-core (~> 3.11) - rspec-expectations (~> 3.11) - rspec-mocks (~> 3.11) - rspec-support (~> 3.11) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) rspec-sidekiq (3.1.0) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.12.0) rspec_chunked (0.6) - rubocop (1.50.2) + rubocop (1.51.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.2.0.0) @@ -611,11 +611,11 @@ GEM rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.28.0) + rubocop-ast (1.28.1) parser (>= 3.2.1.0) rubocop-capybara (2.18.0) rubocop (~> 1.41) - rubocop-performance (1.17.1) + rubocop-performance (1.18.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) rubocop-rails (2.19.1) @@ -761,7 +761,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.7) + zeitwerk (2.6.8) PLATFORMS ruby @@ -770,7 +770,7 @@ DEPENDENCIES active_model_serializers (~> 0.10) addressable (~> 2.8) annotate (~> 3.2) - aws-sdk-s3 (~> 1.120) + aws-sdk-s3 (~> 1.122) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) @@ -860,7 +860,7 @@ DEPENDENCIES redcarpet (~> 3.6) redis (~> 4.5) redis-namespace (~> 1.10) - rqrcode (~> 2.1) + rqrcode (~> 2.2) rspec-rails (~> 6.0) rspec-sidekiq (~> 3.1) rspec_chunked (~> 0.6) diff --git a/app/controllers/api/v1/featured_tags_controller.rb b/app/controllers/api/v1/featured_tags_controller.rb index edb42a94e..5c81877bd 100644 --- a/app/controllers/api/v1/featured_tags_controller.rb +++ b/app/controllers/api/v1/featured_tags_controller.rb @@ -13,7 +13,7 @@ class Api::V1::FeaturedTagsController < Api::BaseController end def create - featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name]) + featured_tag = CreateFeaturedTagService.new.call(current_account, params.require(:name)) render json: featured_tag, serializer: REST::FeaturedTagSerializer end @@ -33,6 +33,6 @@ class Api::V1::FeaturedTagsController < Api::BaseController end def featured_tag_params - params.permit(:name) + params.require(:name) end end diff --git a/app/javascript/mastodon/components/admin/Counter.jsx b/app/javascript/mastodon/components/admin/Counter.jsx index 5a5b2b869..569f8628a 100644 --- a/app/javascript/mastodon/components/admin/Counter.jsx +++ b/app/javascript/mastodon/components/admin/Counter.jsx @@ -4,7 +4,7 @@ import api from 'mastodon/api'; import { FormattedNumber } from 'react-intl'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; import classNames from 'classnames'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; const percIncrease = (a, b) => { let percent; diff --git a/app/javascript/mastodon/components/admin/Dimension.jsx b/app/javascript/mastodon/components/admin/Dimension.jsx index 977c8208d..3005c15ae 100644 --- a/app/javascript/mastodon/components/admin/Dimension.jsx +++ b/app/javascript/mastodon/components/admin/Dimension.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import api from 'mastodon/api'; import { FormattedNumber } from 'react-intl'; import { roundTo10 } from 'mastodon/utils/numbers'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; export default class Dimension extends React.PureComponent { diff --git a/app/javascript/mastodon/components/display_name.tsx b/app/javascript/mastodon/components/display_name.tsx index ce435066d..c537cd24c 100644 --- a/app/javascript/mastodon/components/display_name.tsx +++ b/app/javascript/mastodon/components/display_name.tsx @@ -5,7 +5,7 @@ import type { List } from 'immutable'; import type { Account } from '../../types/resources'; import { autoPlayGif } from '../initial_state'; -import Skeleton from './skeleton'; +import { Skeleton } from './skeleton'; interface Props { account?: Account; diff --git a/app/javascript/mastodon/components/empty_account.tsx b/app/javascript/mastodon/components/empty_account.tsx index 3adb5b20f..a4a6b7f82 100644 --- a/app/javascript/mastodon/components/empty_account.tsx +++ b/app/javascript/mastodon/components/empty_account.tsx @@ -3,7 +3,7 @@ import React from 'react'; import classNames from 'classnames'; import { DisplayName } from 'mastodon/components/display_name'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; interface Props { size?: number; diff --git a/app/javascript/mastodon/components/hashtag.jsx b/app/javascript/mastodon/components/hashtag.jsx index d03b1a45a..3efd679a5 100644 --- a/app/javascript/mastodon/components/hashtag.jsx +++ b/app/javascript/mastodon/components/hashtag.jsx @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Link } from 'react-router-dom'; import ShortNumber from 'mastodon/components/short_number'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; import classNames from 'classnames'; class SilentErrorBoundary extends React.Component { diff --git a/app/javascript/mastodon/components/server_banner.jsx b/app/javascript/mastodon/components/server_banner.jsx index 9669decc8..6c3abd5b6 100644 --- a/app/javascript/mastodon/components/server_banner.jsx +++ b/app/javascript/mastodon/components/server_banner.jsx @@ -4,7 +4,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { fetchServer } from 'mastodon/actions/server'; import ShortNumber from 'mastodon/components/short_number'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; import Account from 'mastodon/containers/account_container'; import { domain } from 'mastodon/initial_state'; import { ServerHeroImage } from 'mastodon/components/server_hero_image'; diff --git a/app/javascript/mastodon/components/skeleton.jsx b/app/javascript/mastodon/components/skeleton.jsx deleted file mode 100644 index 6a17ffb26..000000000 --- a/app/javascript/mastodon/components/skeleton.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const Skeleton = ({ width, height }) => ; - -Skeleton.propTypes = { - width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), -}; - -export default Skeleton; diff --git a/app/javascript/mastodon/components/skeleton.tsx b/app/javascript/mastodon/components/skeleton.tsx new file mode 100644 index 000000000..8d43e6827 --- /dev/null +++ b/app/javascript/mastodon/components/skeleton.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +interface Props { + width?: number | string; + height?: number | string; +} + +export const Skeleton: React.FC = ({ width, height }) => ( + + ‌ + +); diff --git a/app/javascript/mastodon/components/timeline_hint.jsx b/app/javascript/mastodon/components/timeline_hint.jsx deleted file mode 100644 index ac9a79dcc..000000000 --- a/app/javascript/mastodon/components/timeline_hint.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; - -const TimelineHint = ({ resource, url }) => ( -
- -
- -
-); - -TimelineHint.propTypes = { - resource: PropTypes.node.isRequired, - url: PropTypes.string.isRequired, -}; - -export default TimelineHint; diff --git a/app/javascript/mastodon/components/timeline_hint.tsx b/app/javascript/mastodon/components/timeline_hint.tsx new file mode 100644 index 000000000..712b4c293 --- /dev/null +++ b/app/javascript/mastodon/components/timeline_hint.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { FormattedMessage } from 'react-intl'; + +interface Props { + resource: JSX.Element; + url: string; +} + +export const TimelineHint: React.FC = ({ resource, url }) => ( +
+ + + +
+ + + +
+); diff --git a/app/javascript/mastodon/features/about/index.jsx b/app/javascript/mastodon/features/about/index.jsx index f025a9633..61a9180a1 100644 --- a/app/javascript/mastodon/features/about/index.jsx +++ b/app/javascript/mastodon/features/about/index.jsx @@ -8,7 +8,7 @@ import LinkFooter from 'mastodon/features/ui/components/link_footer'; import { Helmet } from 'react-helmet'; import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server'; import Account from 'mastodon/containers/account_container'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; import { Icon } from 'mastodon/components/icon'; import classNames from 'classnames'; import { ServerHeroImage } from 'mastodon/components/server_hero_image'; diff --git a/app/javascript/mastodon/features/account_timeline/index.jsx b/app/javascript/mastodon/features/account_timeline/index.jsx index 2a0530526..ce9485216 100644 --- a/app/javascript/mastodon/features/account_timeline/index.jsx +++ b/app/javascript/mastodon/features/account_timeline/index.jsx @@ -12,7 +12,7 @@ import ColumnBackButton from '../../components/column_back_button'; import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; -import TimelineHint from 'mastodon/components/timeline_hint'; +import { TimelineHint } from 'mastodon/components/timeline_hint'; import { me } from 'mastodon/initial_state'; import LimitedAccountHint from './components/limited_account_hint'; import { getAccountHidden } from 'mastodon/selectors'; diff --git a/app/javascript/mastodon/features/explore/components/story.jsx b/app/javascript/mastodon/features/explore/components/story.jsx index c7320c886..6e8db6230 100644 --- a/app/javascript/mastodon/features/explore/components/story.jsx +++ b/app/javascript/mastodon/features/explore/components/story.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Blurhash } from 'mastodon/components/blurhash'; import { accountsCountRenderer } from 'mastodon/components/hashtag'; import ShortNumber from 'mastodon/components/short_number'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; import classNames from 'classnames'; export default class Story extends React.PureComponent { diff --git a/app/javascript/mastodon/features/followers/index.jsx b/app/javascript/mastodon/features/followers/index.jsx index cdd65c9ef..1a1fdf578 100644 --- a/app/javascript/mastodon/features/followers/index.jsx +++ b/app/javascript/mastodon/features/followers/index.jsx @@ -17,7 +17,7 @@ import Column from '../ui/components/column'; import HeaderContainer from '../account_timeline/containers/header_container'; import ColumnBackButton from '../../components/column_back_button'; import ScrollableList from '../../components/scrollable_list'; -import TimelineHint from 'mastodon/components/timeline_hint'; +import { TimelineHint } from 'mastodon/components/timeline_hint'; import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; import { getAccountHidden } from 'mastodon/selectors'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; diff --git a/app/javascript/mastodon/features/following/index.jsx b/app/javascript/mastodon/features/following/index.jsx index 26dee213d..c024ff063 100644 --- a/app/javascript/mastodon/features/following/index.jsx +++ b/app/javascript/mastodon/features/following/index.jsx @@ -17,7 +17,7 @@ import Column from '../ui/components/column'; import HeaderContainer from '../account_timeline/containers/header_container'; import ColumnBackButton from '../../components/column_back_button'; import ScrollableList from '../../components/scrollable_list'; -import TimelineHint from 'mastodon/components/timeline_hint'; +import { TimelineHint } from 'mastodon/components/timeline_hint'; import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; import { getAccountHidden } from 'mastodon/selectors'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; diff --git a/app/javascript/mastodon/features/privacy_policy/index.jsx b/app/javascript/mastodon/features/privacy_policy/index.jsx index d5bbda6a3..07a691498 100644 --- a/app/javascript/mastodon/features/privacy_policy/index.jsx +++ b/app/javascript/mastodon/features/privacy_policy/index.jsx @@ -4,7 +4,7 @@ import { Helmet } from 'react-helmet'; import { FormattedMessage, FormattedDate, injectIntl, defineMessages } from 'react-intl'; import Column from 'mastodon/components/column'; import api from 'mastodon/api'; -import Skeleton from 'mastodon/components/skeleton'; +import { Skeleton } from 'mastodon/components/skeleton'; const messages = defineMessages({ title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' }, diff --git a/app/javascript/mastodon/features/ui/components/embed_modal.jsx b/app/javascript/mastodon/features/ui/components/embed_modal.jsx index 3e0bcc93c..949c64042 100644 --- a/app/javascript/mastodon/features/ui/components/embed_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/embed_modal.jsx @@ -85,7 +85,7 @@ class EmbedModal extends ImmutablePureComponent { className='embed-modal__iframe' frameBorder='0' ref={this.setIframeRef} - sandbox='allow-same-origin' + sandbox='allow-scripts allow-same-origin' title='preview' /> diff --git a/app/lib/admin/metrics/dimension.rb b/app/lib/admin/metrics/dimension.rb index 81b89d9b3..e0122a65b 100644 --- a/app/lib/admin/metrics/dimension.rb +++ b/app/lib/admin/metrics/dimension.rb @@ -14,9 +14,9 @@ class Admin::Metrics::Dimension }.freeze def self.retrieve(dimension_keys, start_at, end_at, limit, params) - Array(dimension_keys).map do |key| + Array(dimension_keys).filter_map do |key| klass = DIMENSIONS[key.to_sym] klass&.new(start_at, end_at, limit, klass.with_params? ? params.require(key.to_sym) : nil) - end.compact + end end end diff --git a/app/lib/admin/metrics/measure.rb b/app/lib/admin/metrics/measure.rb index 0b510eb25..fe7e04929 100644 --- a/app/lib/admin/metrics/measure.rb +++ b/app/lib/admin/metrics/measure.rb @@ -19,9 +19,9 @@ class Admin::Metrics::Measure }.freeze def self.retrieve(measure_keys, start_at, end_at, params) - Array(measure_keys).map do |key| + Array(measure_keys).filter_map do |key| klass = MEASURES[key.to_sym] klass&.new(start_at, end_at, klass.with_params? ? params.require(key.to_sym) : nil) - end.compact + end end end diff --git a/app/lib/extractor.rb b/app/lib/extractor.rb index 540bbe1a9..9090773ae 100644 --- a/app/lib/extractor.rb +++ b/app/lib/extractor.rb @@ -64,7 +64,7 @@ module Extractor end_position = match_data.char_end(1) after = ::Regexp.last_match.post_match - if %r{\A://}.match?(after) + if after.start_with?('://') hash_text.match(/(.+)(https?\Z)/) do |matched| hash_text = matched[1] end_position -= matched[2].codepoint_length diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index e98ae2d70..3cf6e21c3 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -213,7 +213,7 @@ class FeedManager timeline_key = key(:home, account.id) timeline_status_ids = redis.zrange(timeline_key, 0, -1) statuses = Status.where(id: timeline_status_ids).select(:id, :reblog_of_id, :account_id).to_a - reblogged_ids = Status.where(id: statuses.map(&:reblog_of_id).compact, account: target_account).pluck(:id) + reblogged_ids = Status.where(id: statuses.filter_map(&:reblog_of_id), account: target_account).pluck(:id) with_mentions_ids = Mention.active.where(status_id: statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact, account: target_account).pluck(:status_id) target_statuses = statuses.select do |status| @@ -233,7 +233,7 @@ class FeedManager timeline_key = key(:list, list.id) timeline_status_ids = redis.zrange(timeline_key, 0, -1) statuses = Status.where(id: timeline_status_ids).select(:id, :reblog_of_id, :account_id).to_a - reblogged_ids = Status.where(id: statuses.map(&:reblog_of_id).compact, account: target_account).pluck(:id) + reblogged_ids = Status.where(id: statuses.filter_map(&:reblog_of_id), account: target_account).pluck(:id) with_mentions_ids = Mention.active.where(status_id: statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact, account: target_account).pluck(:status_id) target_statuses = statuses.select do |status| @@ -603,9 +603,9 @@ class FeedManager arr end - crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:in_reply_to_account_id).compact).pluck(:target_account_id).index_with(true) + crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true) crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h - crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.map { |s| s.account_id if s.reblog? }.compact, show_reblogs: false).pluck(:target_account_id).index_with(true) + crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true) crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true) diff --git a/app/models/account.rb b/app/models/account.rb index 30af67615..bbc9bda02 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -299,11 +299,11 @@ class Account < ApplicationRecord end def fields - (self[:fields] || []).map do |f| + (self[:fields] || []).filter_map do |f| Account::Field.new(self, f) rescue nil - end.compact + end end def fields_attributes=(attributes) diff --git a/app/models/account_statuses_cleanup_policy.rb b/app/models/account_statuses_cleanup_policy.rb index 63582b9f9..a10279544 100644 --- a/app/models/account_statuses_cleanup_policy.rb +++ b/app/models/account_statuses_cleanup_policy.rb @@ -117,12 +117,12 @@ class AccountStatusesCleanupPolicy < ApplicationRecord private def update_last_inspected - if EXCEPTION_BOOLS.map { |name| attribute_change_to_be_saved(name) }.compact.include?([true, false]) + if EXCEPTION_BOOLS.filter_map { |name| attribute_change_to_be_saved(name) }.include?([true, false]) # Policy has been widened in such a way that any previously-inspected status # may need to be deleted, so we'll have to start again. redis.del("account_cleanup:#{account_id}") end - redis.del("account_cleanup:#{account_id}") if EXCEPTION_THRESHOLDS.map { |name| attribute_change_to_be_saved(name) }.compact.any? { |old, new| old.present? && (new.nil? || new > old) } + redis.del("account_cleanup:#{account_id}") if EXCEPTION_THRESHOLDS.filter_map { |name| attribute_change_to_be_saved(name) }.any? { |old, new| old.present? && (new.nil? || new > old) } end def validate_local_account diff --git a/app/models/account_suggestions/setting_source.rb b/app/models/account_suggestions/setting_source.rb index 7b8873e0c..6185732b4 100644 --- a/app/models/account_suggestions/setting_source.rb +++ b/app/models/account_suggestions/setting_source.rb @@ -48,14 +48,14 @@ class AccountSuggestions::SettingSource < AccountSuggestions::Source end def setting_to_usernames_and_domains - setting.split(',').map do |str| + setting.split(',').filter_map do |str| username, domain = str.strip.gsub(/\A@/, '').split('@', 2) domain = nil if TagManager.instance.local_domain?(domain) next if username.blank? [username.downcase, domain&.downcase] - end.compact + end end def setting diff --git a/app/models/account_suggestions/source.rb b/app/models/account_suggestions/source.rb index be462cd0f..504d26a8b 100644 --- a/app/models/account_suggestions/source.rb +++ b/app/models/account_suggestions/source.rb @@ -20,7 +20,7 @@ class AccountSuggestions::Source map = scope.index_by { |account| to_ordered_list_key(account) } - ordered_list.map { |ordered_list_key| map[ordered_list_key] }.compact.map do |account| + ordered_list.filter_map { |ordered_list_key| map[ordered_list_key] }.map do |account| AccountSuggestions::Suggestion.new( account: account, source: key diff --git a/app/models/follow_recommendation_filter.rb b/app/models/follow_recommendation_filter.rb index 531332614..2fab97569 100644 --- a/app/models/follow_recommendation_filter.rb +++ b/app/models/follow_recommendation_filter.rb @@ -22,7 +22,7 @@ class FollowRecommendationFilter account_ids = redis.zrevrange("follow_recommendations:#{@language}", 0, -1).map(&:to_i) accounts = Account.where(id: account_ids).index_by(&:id) - account_ids.map { |id| accounts[id] }.compact + account_ids.filter_map { |id| accounts[id] } end end end diff --git a/app/models/notification.rb b/app/models/notification.rb index 8ba506fa1..5527953af 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -114,7 +114,7 @@ class Notification < ApplicationRecord ActiveRecord::Associations::Preloader.new.preload(grouped_notifications, associations) end - unique_target_statuses = notifications.map(&:target_status).compact.uniq + unique_target_statuses = notifications.filter_map(&:target_status).uniq # Call cache_collection in block cached_statuses_by_id = yield(unique_target_statuses).index_by(&:id) diff --git a/app/models/user_role.rb b/app/models/user_role.rb index a1b91dc0f..5472646c6 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -125,7 +125,7 @@ class UserRole < ApplicationRecord end def permissions_as_keys=(value) - self.permissions = value.map(&:presence).compact.reduce(Flags::NONE) { |bitmask, privilege| FLAGS.key?(privilege.to_sym) ? (bitmask | FLAGS[privilege.to_sym]) : bitmask } + self.permissions = value.filter_map(&:presence).reduce(Flags::NONE) { |bitmask, privilege| FLAGS.key?(privilege.to_sym) ? (bitmask | FLAGS[privilege.to_sym]) : bitmask } end def can?(*any_of_privileges) diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 9a056a386..c556bcc2b 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -53,7 +53,7 @@ class Webhook < ApplicationRecord end def strip_events - self.events = events.map { |str| str.strip.presence }.compact if events.present? + self.events = events.filter_map { |str| str.strip.presence } if events.present? end def generate_secret diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index b3b279147..f3fbb8021 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -68,7 +68,7 @@ class ProcessMentionsService < BaseService def assign_mentions! # Make sure we never mention blocked accounts unless @current_mentions.empty? - mentioned_domains = @current_mentions.map { |m| m.account.domain }.compact.uniq + mentioned_domains = @current_mentions.filter_map { |m| m.account.domain }.uniq blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains)) mentioned_account_ids = @current_mentions.map(&:account_id) blocked_account_ids = Set.new(@status.account.block_relationships.where(target_account_id: mentioned_account_ids).pluck(:target_account_id)) diff --git a/app/validators/existing_username_validator.rb b/app/validators/existing_username_validator.rb index 45de4f4a4..037d92f39 100644 --- a/app/validators/existing_username_validator.rb +++ b/app/validators/existing_username_validator.rb @@ -4,14 +4,14 @@ class ExistingUsernameValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) return if value.blank? - usernames_and_domains = value.split(',').map do |str| + usernames_and_domains = value.split(',').filter_map do |str| username, domain = str.strip.gsub(/\A@/, '').split('@', 2) domain = nil if TagManager.instance.local_domain?(domain) next if username.blank? [str, username, domain] - end.compact + end usernames_with_no_accounts = usernames_and_domains.filter_map do |(str, username, domain)| str unless Account.find_remote(username, domain) diff --git a/app/views/auth/confirmations/captcha.html.haml b/app/views/auth/confirmations/captcha.html.haml index 1f577383e..77f4b35b4 100644 --- a/app/views/auth/confirmations/captcha.html.haml +++ b/app/views/auth/confirmations/captcha.html.haml @@ -5,6 +5,7 @@ = render 'auth/shared/progress', stage: 'confirm' = hidden_field_tag :confirmation_token, params[:confirmation_token] + = hidden_field_tag :redirect_to_app, params[:redirect_to_app] %p.lead= t('auth.captcha_confirmation.hint_html') diff --git a/db/migrate/20200407202420_migrate_unavailable_inboxes.rb b/db/migrate/20200407202420_migrate_unavailable_inboxes.rb index 8f9c68794..05a01be28 100644 --- a/db/migrate/20200407202420_migrate_unavailable_inboxes.rb +++ b/db/migrate/20200407202420_migrate_unavailable_inboxes.rb @@ -5,9 +5,9 @@ class MigrateUnavailableInboxes < ActiveRecord::Migration[5.2] redis = RedisConfiguration.pool.checkout urls = redis.smembers('unavailable_inboxes') - hosts = urls.map do |url| + hosts = urls.filter_map do |url| Addressable::URI.parse(url).normalized_host - end.compact.uniq + end.uniq UnavailableDomain.delete_all diff --git a/package.json b/package.json index ba4f66c6d..7fd1273a2 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "file-loader": "^6.2.0", "font-awesome": "^4.7.0", "fuzzysort": "^2.0.4", - "glob": "^10.2.2", + "glob": "^10.2.6", "history": "^4.10.1", "http-link-header": "^1.1.1", "immutable": "^4.3.0", @@ -116,7 +116,7 @@ "regenerator-runtime": "^0.13.11", "requestidlecallback": "^0.3.0", "reselect": "^4.1.8", - "rimraf": "^5.0.0", + "rimraf": "^5.0.1", "sass": "^1.62.1", "sass-loader": "^10.2.0", "stacktrace-js": "^2.0.2", @@ -131,7 +131,7 @@ "webpack-assets-manifest": "^4.0.6", "webpack-bundle-analyzer": "^4.8.0", "webpack-cli": "^3.3.12", - "webpack-merge": "^5.8.0", + "webpack-merge": "^5.9.0", "wicg-inert": "^3.1.2", "workbox-expiration": "^6.5.4", "workbox-precaching": "^6.5.4", @@ -178,8 +178,8 @@ "@types/uuid": "^9.0.0", "@types/webpack": "^4.41.33", "@types/yargs": "^17.0.24", - "@typescript-eslint/eslint-plugin": "^5.59.6", - "@typescript-eslint/parser": "^5.59.6", + "@typescript-eslint/eslint-plugin": "^5.59.7", + "@typescript-eslint/parser": "^5.59.7", "babel-jest": "^29.5.0", "eslint": "^8.40.0", "eslint-config-prettier": "^8.8.0", @@ -199,7 +199,7 @@ "prettier": "^2.8.8", "react-intl-translations-manager": "^5.0.3", "react-test-renderer": "^18.2.0", - "stylelint": "^15.6.1", + "stylelint": "^15.6.2", "stylelint-config-standard-scss": "^9.0.0", "typescript": "^5.0.4", "webpack-dev-server": "^3.11.3", @@ -216,7 +216,7 @@ }, "lint-staged": { "*": "prettier --ignore-unknown --write", - "Capfile|Gemfile|*.{rb,ruby,ru,rake}": "bundle exec rubocop -a", + "Capfile|Gemfile|*.{rb,ruby,ru,rake}": "bundle exec rubocop --force-exclusion -a", "*.{js,jsx,ts,tsx}": "eslint --fix", "*.{css,scss}": "stylelint --fix" } diff --git a/spec/controllers/admin/announcements_controller_spec.rb b/spec/controllers/admin/announcements_controller_spec.rb index 288ac1d71..a8905160f 100644 --- a/spec/controllers/admin/announcements_controller_spec.rb +++ b/spec/controllers/admin/announcements_controller_spec.rb @@ -18,4 +18,59 @@ describe Admin::AnnouncementsController do expect(response).to have_http_status(:success) end end + + describe 'GET #new' do + it 'returns http success and renders new' do + get :new + + expect(response).to have_http_status(:success) + expect(response).to render_template(:new) + end + end + + describe 'GET #edit' do + let(:announcement) { Fabricate(:announcement) } + + it 'returns http success and renders edit' do + get :edit, params: { id: announcement.id } + + expect(response).to have_http_status(:success) + expect(response).to render_template(:edit) + end + end + + describe 'POST #create' do + it 'creates a new announcement and redirects' do + expect do + post :create, params: { announcement: { text: 'The announcement message.' } } + end.to change(Announcement, :count).by(1) + + expect(response).to redirect_to(admin_announcements_path) + expect(flash.notice).to match(I18n.t('admin.announcements.published_msg')) + end + end + + describe 'PUT #update' do + let(:announcement) { Fabricate(:announcement, text: 'Original text') } + + it 'updates an announcement and redirects' do + put :update, params: { id: announcement.id, announcement: { text: 'Updated text.' } } + + expect(response).to redirect_to(admin_announcements_path) + expect(flash.notice).to match(I18n.t('admin.announcements.updated_msg')) + end + end + + describe 'DELETE #destroy' do + let!(:announcement) { Fabricate(:announcement, text: 'Original text') } + + it 'destroys an announcement and redirects' do + expect do + delete :destroy, params: { id: announcement.id } + end.to change(Announcement, :count).by(-1) + + expect(response).to redirect_to(admin_announcements_path) + expect(flash.notice).to match(I18n.t('admin.announcements.destroyed_msg')) + end + end end diff --git a/spec/controllers/api/v1/featured_tags_controller_spec.rb b/spec/controllers/api/v1/featured_tags_controller_spec.rb deleted file mode 100644 index aac942901..000000000 --- a/spec/controllers/api/v1/featured_tags_controller_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Api::V1::FeaturedTagsController do - render_views - - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } - let(:account) { Fabricate(:account) } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #index' do - it 'returns http success' do - get :index, params: { account_id: account.id, limit: 2 } - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/fabricators/featured_tag_fabricator.rb b/spec/fabricators/featured_tag_fabricator.rb index 747d8e36a..838364056 100644 --- a/spec/fabricators/featured_tag_fabricator.rb +++ b/spec/fabricators/featured_tag_fabricator.rb @@ -3,5 +3,5 @@ Fabricator(:featured_tag) do account tag - name 'Tag' + name { sequence(:name) { |i| "Tag#{i}" } } end diff --git a/spec/features/captcha_spec.rb b/spec/features/captcha_spec.rb new file mode 100644 index 000000000..db89ff3e6 --- /dev/null +++ b/spec/features/captcha_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'email confirmation flow when captcha is enabled' do + let(:user) { Fabricate(:user, confirmed_at: nil, confirmation_token: 'foobar', created_by_application: client_app) } + let(:client_app) { nil } + + before do + # rubocop:disable RSpec/AnyInstance -- easiest way to deal with that that I know of + allow_any_instance_of(Auth::ConfirmationsController).to receive(:captcha_enabled?).and_return(true) + allow_any_instance_of(Auth::ConfirmationsController).to receive(:check_captcha!).and_return(true) + allow_any_instance_of(Auth::ConfirmationsController).to receive(:render_captcha).and_return(nil) + # rubocop:enable RSpec/AnyInstance + end + + context 'when the user signed up through an app' do + let(:client_app) { Fabricate(:application) } + + it 'logs in' do + visit "/auth/confirmation?confirmation_token=#{user.confirmation_token}&redirect_to_app=true" + + # It presents the user with a captcha form + expect(page).to have_title(I18n.t('auth.captcha_confirmation.title')) + + # It does not confirm the user just yet + expect(user.reload.confirmed?).to be false + + # It redirects to app and confirms user + click_on I18n.t('challenge.confirm') + expect(user.reload.confirmed?).to be true + expect(page).to have_current_path(/\A#{client_app.confirmation_redirect_uri}/, url: true) + end + end +end diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb index 38b9c4fdb..a278bb4e8 100644 --- a/spec/policies/status_policy_spec.rb +++ b/spec/policies/status_policy_spec.rb @@ -11,75 +11,79 @@ RSpec.describe StatusPolicy, type: :model do let(:bob) { Fabricate(:account, username: 'bob') } let(:status) { Fabricate(:status, account: alice) } - permissions :show?, :reblog? do - it 'grants access when no viewer' do - expect(subject).to permit(nil, status) - end + context 'with the permissions of show? and reblog?' do + permissions :show?, :reblog? do + it 'grants access when no viewer' do + expect(subject).to permit(nil, status) + end - it 'denies access when viewer is blocked' do - block = Fabricate(:block) - status.visibility = :private - status.account = block.target_account + it 'denies access when viewer is blocked' do + block = Fabricate(:block) + status.visibility = :private + status.account = block.target_account - expect(subject).to_not permit(block.account, status) + expect(subject).to_not permit(block.account, status) + end end end - permissions :show? do - it 'grants access when direct and account is viewer' do - status.visibility = :direct + context 'with the permission of show?' do + permissions :show? do + it 'grants access when direct and account is viewer' do + status.visibility = :direct - expect(subject).to permit(status.account, status) - end + expect(subject).to permit(status.account, status) + end - it 'grants access when direct and viewer is mentioned' do - status.visibility = :direct - status.mentions = [Fabricate(:mention, account: alice)] + it 'grants access when direct and viewer is mentioned' do + status.visibility = :direct + status.mentions = [Fabricate(:mention, account: alice)] - expect(subject).to permit(alice, status) - end + expect(subject).to permit(alice, status) + end - it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do - status.visibility = :direct - status.mentions = [Fabricate(:mention, account: bob)] - status.mentions.load + it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do + status.visibility = :direct + status.mentions = [Fabricate(:mention, account: bob)] + status.mentions.load - expect(subject).to permit(bob, status) - end + expect(subject).to permit(bob, status) + end - it 'denies access when direct and viewer is not mentioned' do - viewer = Fabricate(:account) - status.visibility = :direct + it 'denies access when direct and viewer is not mentioned' do + viewer = Fabricate(:account) + status.visibility = :direct - expect(subject).to_not permit(viewer, status) - end + expect(subject).to_not permit(viewer, status) + end - it 'grants access when private and account is viewer' do - status.visibility = :private + it 'grants access when private and account is viewer' do + status.visibility = :private - expect(subject).to permit(status.account, status) - end + expect(subject).to permit(status.account, status) + end - it 'grants access when private and account is following viewer' do - follow = Fabricate(:follow) - status.visibility = :private - status.account = follow.target_account + it 'grants access when private and account is following viewer' do + follow = Fabricate(:follow) + status.visibility = :private + status.account = follow.target_account - expect(subject).to permit(follow.account, status) - end + expect(subject).to permit(follow.account, status) + end - it 'grants access when private and viewer is mentioned' do - status.visibility = :private - status.mentions = [Fabricate(:mention, account: alice)] + it 'grants access when private and viewer is mentioned' do + status.visibility = :private + status.mentions = [Fabricate(:mention, account: alice)] - expect(subject).to permit(alice, status) - end + expect(subject).to permit(alice, status) + end - it 'denies access when private and viewer is not mentioned or followed' do - viewer = Fabricate(:account) - status.visibility = :private + it 'denies access when private and viewer is not mentioned or followed' do + viewer = Fabricate(:account) + status.visibility = :private - expect(subject).to_not permit(viewer, status) + expect(subject).to_not permit(viewer, status) + end end it 'denies access when local-only and the viewer is not logged in' do @@ -95,55 +99,63 @@ RSpec.describe StatusPolicy, type: :model do end end - permissions :reblog? do - it 'denies access when private' do - viewer = Fabricate(:account) - status.visibility = :private + context 'with the permission of reblog?' do + permissions :reblog? do + it 'denies access when private' do + viewer = Fabricate(:account) + status.visibility = :private - expect(subject).to_not permit(viewer, status) - end + expect(subject).to_not permit(viewer, status) + end - it 'denies access when direct' do - viewer = Fabricate(:account) - status.visibility = :direct + it 'denies access when direct' do + viewer = Fabricate(:account) + status.visibility = :direct - expect(subject).to_not permit(viewer, status) + expect(subject).to_not permit(viewer, status) + end end end - permissions :destroy?, :unreblog? do - it 'grants access when account is deleter' do - expect(subject).to permit(status.account, status) - end + context 'with the permissions of destroy? and unreblog?' do + permissions :destroy?, :unreblog? do + it 'grants access when account is deleter' do + expect(subject).to permit(status.account, status) + end - it 'denies access when account is not deleter' do - expect(subject).to_not permit(bob, status) - end + it 'denies access when account is not deleter' do + expect(subject).to_not permit(bob, status) + end - it 'denies access when no deleter' do - expect(subject).to_not permit(nil, status) + it 'denies access when no deleter' do + expect(subject).to_not permit(nil, status) + end end end - permissions :favourite? do - it 'grants access when viewer is not blocked' do - follow = Fabricate(:follow) - status.account = follow.target_account + context 'with the permission of favourite?' do + permissions :favourite? do + it 'grants access when viewer is not blocked' do + follow = Fabricate(:follow) + status.account = follow.target_account - expect(subject).to permit(follow.account, status) - end + expect(subject).to permit(follow.account, status) + end - it 'denies when viewer is blocked' do - block = Fabricate(:block) - status.account = block.target_account + it 'denies when viewer is blocked' do + block = Fabricate(:block) + status.account = block.target_account - expect(subject).to_not permit(block.account, status) + expect(subject).to_not permit(block.account, status) + end end end - permissions :update? do - it 'grants access if owner' do - expect(subject).to permit(status.account, status) + context 'with the permission of update?' do + permissions :update? do + it 'grants access if owner' do + expect(subject).to permit(status.account, status) + end end end end diff --git a/spec/presenters/status_relationships_presenter_spec.rb b/spec/presenters/status_relationships_presenter_spec.rb index 11116cabd..a62fa004a 100644 --- a/spec/presenters/status_relationships_presenter_spec.rb +++ b/spec/presenters/status_relationships_presenter_spec.rb @@ -15,7 +15,7 @@ RSpec.describe StatusRelationshipsPresenter do let(:presenter) { StatusRelationshipsPresenter.new(statuses, current_account_id, **options) } let(:current_account_id) { Fabricate(:account).id } let(:statuses) { [Fabricate(:status)] } - let(:status_ids) { statuses.map(&:id) + statuses.map(&:reblog_of_id).compact } + let(:status_ids) { statuses.map(&:id) + statuses.filter_map(&:reblog_of_id) } let(:default_map) { { 1 => true } } context 'when options are not set' do diff --git a/spec/requests/api/v1/featured_tags_spec.rb b/spec/requests/api/v1/featured_tags_spec.rb new file mode 100644 index 000000000..8a552c1d4 --- /dev/null +++ b/spec/requests/api/v1/featured_tags_spec.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'FeaturedTags' do + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:scopes) { 'read:accounts write:accounts' } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + 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 + + describe 'GET /api/v1/featured_tags' do + context 'with wrong scope' do + before do + get '/api/v1/featured_tags', headers: headers + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'when Authorization header is missing' do + it 'returns http unauthorized' do + get '/api/v1/featured_tags' + + expect(response).to have_http_status(401) + end + end + + it 'returns http success' do + get '/api/v1/featured_tags', headers: headers + + expect(response).to have_http_status(200) + end + + context 'when the requesting user has no featured tag' do + before { Fabricate.times(3, :featured_tag) } + + it 'returns an empty body' do + get '/api/v1/featured_tags', headers: headers + + body = body_as_json + + expect(body).to be_empty + end + end + + context 'when the requesting user has featured tags' do + let!(:user_featured_tags) { Fabricate.times(5, :featured_tag, account: user.account) } + + it 'returns only the featured tags belonging to the requesting user' do + get '/api/v1/featured_tags', headers: headers + + body = body_as_json + expected_ids = user_featured_tags.pluck(:id).map(&:to_s) + + expect(body.pluck(:id)).to match_array(expected_ids) + end + end + end + + describe 'POST /api/v1/featured_tags' do + let(:params) { { name: 'tag' } } + + it 'returns http success' do + post '/api/v1/featured_tags', headers: headers, params: params + + expect(response).to have_http_status(200) + end + + it 'returns the correct tag name' do + post '/api/v1/featured_tags', headers: headers, params: params + + body = body_as_json + + expect(body[:name]).to eq(params[:name]) + end + + it 'creates a new featured tag for the requesting user' do + post '/api/v1/featured_tags', headers: headers, params: params + + featured_tag = FeaturedTag.find_by(name: params[:name], account: user.account) + + expect(featured_tag).to be_present + end + + context 'with wrong scope' do + before do + post '/api/v1/featured_tags', headers: headers, params: params + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'when Authorization header is missing' do + it 'returns http unauthorized' do + post '/api/v1/featured_tags', params: params + + expect(response).to have_http_status(401) + end + end + + context 'when required param "name" is not provided' do + it 'returns http bad request' do + post '/api/v1/featured_tags', headers: headers + + expect(response).to have_http_status(400) + end + end + + context 'when provided tag name is invalid' do + let(:params) { { name: 'asj&*!' } } + + it 'returns http unprocessable entity' do + post '/api/v1/featured_tags', headers: headers, params: params + + expect(response).to have_http_status(422) + end + end + + context 'when tag name is already taken' do + before do + FeaturedTag.create(name: params[:name], account: user.account) + end + + it 'returns http unprocessable entity' do + post '/api/v1/featured_tags', headers: headers, params: params + + expect(response).to have_http_status(422) + end + end + end + + describe 'DELETE /api/v1/featured_tags' do + let!(:featured_tag) { FeaturedTag.create(name: 'tag', account: user.account) } + let(:id) { featured_tag.id } + + it 'returns http success' do + delete "/api/v1/featured_tags/#{id}", headers: headers + + expect(response).to have_http_status(200) + end + + it 'returns an empty body' do + delete "/api/v1/featured_tags/#{id}", headers: headers + + body = body_as_json + + expect(body).to be_empty + end + + it 'deletes the featured tag' do + delete "/api/v1/featured_tags/#{id}", headers: headers + + featured_tag = FeaturedTag.find_by(id: id) + + expect(featured_tag).to be_nil + end + + context 'with wrong scope' do + before do + delete "/api/v1/featured_tags/#{id}", headers: headers + end + + it_behaves_like 'forbidden for wrong scope', 'read:statuses' + end + + context 'when Authorization header is missing' do + it 'returns http unauthorized' do + delete "/api/v1/featured_tags/#{id}" + + expect(response).to have_http_status(401) + end + end + + context 'when featured tag with given id does not exist' do + it 'returns http not found' do + delete '/api/v1/featured_tags/0', headers: headers + + expect(response).to have_http_status(404) + end + end + + context 'when deleting a featured tag of another user' do + let!(:other_user_featured_tag) { Fabricate(:featured_tag) } + let(:id) { other_user_featured_tag.id } + + it 'returns http not found' do + delete "/api/v1/featured_tags/#{id}", headers: headers + + expect(response).to have_http_status(404) + end + end + end +end diff --git a/yarn.lock b/yarn.lock index 597a50574..5cd2b4148 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2449,15 +2449,15 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.59.6": - version "5.59.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.6.tgz#a350faef1baa1e961698240f922d8de1761a9e2b" - integrity sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw== +"@typescript-eslint/eslint-plugin@^5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.7.tgz#e470af414f05ecfdc05a23e9ce6ec8f91db56fe2" + integrity sha512-BL+jYxUFIbuYwy+4fF86k5vdT9lT0CNJ6HtwrIvGh0PhH8s0yy5rjaKH2fDCrz5ITHy07WCzVGNvAmjJh4IJFA== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.6" - "@typescript-eslint/type-utils" "5.59.6" - "@typescript-eslint/utils" "5.59.6" + "@typescript-eslint/scope-manager" "5.59.7" + "@typescript-eslint/type-utils" "5.59.7" + "@typescript-eslint/utils" "5.59.7" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" @@ -2465,31 +2465,31 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.59.6": - version "5.59.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.6.tgz#bd36f71f5a529f828e20b627078d3ed6738dbb40" - integrity sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA== +"@typescript-eslint/parser@^5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.7.tgz#02682554d7c1028b89aa44a48bf598db33048caa" + integrity sha512-VhpsIEuq/8i5SF+mPg9jSdIwgMBBp0z9XqjiEay+81PYLJuroN+ET1hM5IhkiYMJd9MkTz8iJLt7aaGAgzWUbQ== dependencies: - "@typescript-eslint/scope-manager" "5.59.6" - "@typescript-eslint/types" "5.59.6" - "@typescript-eslint/typescript-estree" "5.59.6" + "@typescript-eslint/scope-manager" "5.59.7" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/typescript-estree" "5.59.7" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.59.6": - version "5.59.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.6.tgz#d43a3687aa4433868527cfe797eb267c6be35f19" - integrity sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ== +"@typescript-eslint/scope-manager@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz#0243f41f9066f3339d2f06d7f72d6c16a16769e2" + integrity sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ== dependencies: - "@typescript-eslint/types" "5.59.6" - "@typescript-eslint/visitor-keys" "5.59.6" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/visitor-keys" "5.59.7" -"@typescript-eslint/type-utils@5.59.6": - version "5.59.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.6.tgz#37c51d2ae36127d8b81f32a0a4d2efae19277c48" - integrity sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ== +"@typescript-eslint/type-utils@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.7.tgz#89c97291371b59eb18a68039857c829776f1426d" + integrity sha512-ozuz/GILuYG7osdY5O5yg0QxXUAEoI4Go3Do5xeu+ERH9PorHBPSdvD3Tjp2NN2bNLh1NJQSsQu2TPu/Ly+HaQ== dependencies: - "@typescript-eslint/typescript-estree" "5.59.6" - "@typescript-eslint/utils" "5.59.6" + "@typescript-eslint/typescript-estree" "5.59.7" + "@typescript-eslint/utils" "5.59.7" debug "^4.3.4" tsutils "^3.21.0" @@ -2498,10 +2498,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.0.tgz#3fcdac7dbf923ec5251545acdd9f1d42d7c4fe32" integrity sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA== -"@typescript-eslint/types@5.59.6": - version "5.59.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.6.tgz#5a6557a772af044afe890d77c6a07e8c23c2460b" - integrity sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA== +"@typescript-eslint/types@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.7.tgz#6f4857203fceee91d0034ccc30512d2939000742" + integrity sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A== "@typescript-eslint/typescript-estree@5.59.0": version "5.59.0" @@ -2516,30 +2516,30 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.59.6": - version "5.59.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.6.tgz#2fb80522687bd3825504925ea7e1b8de7bb6251b" - integrity sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA== +"@typescript-eslint/typescript-estree@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz#b887acbd4b58e654829c94860dbff4ac55c5cff8" + integrity sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ== dependencies: - "@typescript-eslint/types" "5.59.6" - "@typescript-eslint/visitor-keys" "5.59.6" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/visitor-keys" "5.59.7" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.59.6": - version "5.59.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.6.tgz#82960fe23788113fc3b1f9d4663d6773b7907839" - integrity sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg== +"@typescript-eslint/utils@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.7.tgz#7adf068b136deae54abd9a66ba5a8780d2d0f898" + integrity sha512-yCX9WpdQKaLufz5luG4aJbOpdXf/fjwGMcLFXZVPUz3QqLirG5QcwwnIHNf8cjLjxK4qtzTO8udUtMQSAToQnQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.6" - "@typescript-eslint/types" "5.59.6" - "@typescript-eslint/typescript-estree" "5.59.6" + "@typescript-eslint/scope-manager" "5.59.7" + "@typescript-eslint/types" "5.59.7" + "@typescript-eslint/typescript-estree" "5.59.7" eslint-scope "^5.1.1" semver "^7.3.7" @@ -2551,12 +2551,12 @@ "@typescript-eslint/types" "5.59.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@5.59.6": - version "5.59.6" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.6.tgz#673fccabf28943847d0c8e9e8d008e3ada7be6bb" - integrity sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q== +"@typescript-eslint/visitor-keys@5.59.7": + version "5.59.7" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz#09c36eaf268086b4fbb5eb9dc5199391b6485fc5" + integrity sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ== dependencies: - "@typescript-eslint/types" "5.59.6" + "@typescript-eslint/types" "5.59.7" eslint-visitor-keys "^3.3.0" "@webassemblyjs/ast@1.9.0": @@ -5891,15 +5891,15 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^10.0.0, glob@^10.2.2: - version "10.2.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.2.tgz#ce2468727de7e035e8ecf684669dc74d0526ab75" - integrity sha512-Xsa0BcxIC6th9UwNjZkhrMtNo/MnyRL8jGCP+uEwhA5oFOCY1f2s1/oNKY47xQ0Bg5nkjsfAEIej1VeH62bDDQ== +glob@^10.2.5, glob@^10.2.6: + version "10.2.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.6.tgz#1e27edbb3bbac055cb97113e27a066c100a4e5e1" + integrity sha512-U/rnDpXJGF414QQQZv5uVsabTVxMSwzS5CH0p3DRCIV6ownl4f7PzGnkGmvlum2wB+9RlJWJZ6ACU1INnBqiPA== dependencies: foreground-child "^3.1.0" jackspeak "^2.0.3" - minimatch "^9.0.0" - minipass "^5.0.0" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2" path-scurry "^1.7.0" glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: @@ -8135,10 +8135,10 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.0.tgz#bfc8e88a1c40ffd40c172ddac3decb8451503b56" - integrity sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w== +minimatch@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253" + integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== dependencies: brace-expansion "^2.0.1" @@ -8189,6 +8189,11 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== +"minipass@^5.0.0 || ^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-6.0.2.tgz#542844b6c4ce95b202c0995b0a471f1229de4c81" + integrity sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w== + minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -10162,12 +10167,12 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -rimraf@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.0.tgz#5bda14e410d7e4dd522154891395802ce032c2cb" - integrity sha512-Jf9llaP+RvaEVS5nPShYFhtXIrb3LRKP281ib3So0KkeZKo2wIKyq0Re7TOSwanasA423PSr6CCIL4bP6T040g== +rimraf@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.1.tgz#0881323ab94ad45fec7c0221f27ea1a142f3f0d0" + integrity sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg== dependencies: - glob "^10.0.0" + glob "^10.2.5" ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" @@ -11045,10 +11050,10 @@ stylelint-scss@^4.6.0: postcss-selector-parser "^6.0.11" postcss-value-parser "^4.2.0" -stylelint@^15.6.1: - version "15.6.1" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.6.1.tgz#e4cd33a3af88587b99a5d1328aedd8c298b6dc81" - integrity sha512-d8icFBlVl93Elf3Z5ABQNOCe4nx69is3D/NZhDLAie1eyYnpxfeKe7pCfqzT5W4F8vxHCLSDfV8nKNJzogvV2Q== +stylelint@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.6.2.tgz#06d9005b62a83b72887eed623520e9b472af8c15" + integrity sha512-fjQWwcdUye4DU+0oIxNGwawIPC5DvG5kdObY5Sg4rc87untze3gC/5g/ikePqVjrAsBUZjwMN+pZsAYbDO6ArQ== dependencies: "@csstools/css-parser-algorithms" "^2.1.1" "@csstools/css-tokenizer" "^2.1.1" @@ -11968,10 +11973,10 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" -webpack-merge@^5.8.0: - version "5.8.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" - integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== +webpack-merge@^5.9.0: + version "5.9.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826" + integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg== dependencies: clone-deep "^4.0.1" wildcard "^2.0.0"