diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations-one-step.yml index d7e424a8c..212b2cfe7 100644 --- a/.github/workflows/test-migrations-one-step.yml +++ b/.github/workflows/test-migrations-one-step.yml @@ -23,9 +23,17 @@ jobs: needs: pre_job if: needs.pre_job.outputs.should_skip != 'true' + strategy: + fail-fast: false + + matrix: + postgres: + - 14-alpine + - 15-alpine + services: postgres: - image: postgres:14-alpine + image: postgres:${{ matrix.postgres}} env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml index 25bf5ba87..310153929 100644 --- a/.github/workflows/test-migrations-two-step.yml +++ b/.github/workflows/test-migrations-two-step.yml @@ -23,9 +23,17 @@ jobs: needs: pre_job if: needs.pre_job.outputs.should_skip != 'true' + strategy: + fail-fast: false + + matrix: + postgres: + - 14-alpine + - 15-alpine + services: postgres: - image: postgres:14-alpine + image: postgres:${{ matrix.postgres}} env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0fb99c040..69dd433e3 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -21,12 +21,6 @@ Layout/ArgumentAlignment: - 'config/initializers/cors.rb' - 'config/initializers/session_store.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. -Layout/ExtraSpacing: - Exclude: - - 'config/initializers/omniauth.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # SupportedHashRocketStyles: key, separator, table @@ -39,12 +33,6 @@ Layout/HashAlignment: - 'config/initializers/rack_attack.rb' - 'config/routes.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Width, AllowedPatterns. -Layout/IndentationWidth: - Exclude: - - 'config/initializers/ffmpeg.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment. Layout/LeadingCommentSpace: @@ -52,14 +40,6 @@ Layout/LeadingCommentSpace: - 'config/application.rb' - 'config/initializers/omniauth.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceBeforeBlockBraces: - Exclude: - - 'config/initializers/paperclip.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: require_no_space, require_space @@ -68,19 +48,6 @@ Layout/SpaceInLambdaLiteral: - 'config/environments/production.rb' - 'config/initializers/content_security_policy.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/SpaceInsideStringInterpolation: - Exclude: - - 'config/initializers/webauthn.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowInHeredoc. -Layout/TrailingWhitespace: - Exclude: - - 'config/initializers/paperclip.rb' - # Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Exclude: @@ -621,7 +588,6 @@ RSpec/NoExpectationExample: RSpec/PendingWithoutReason: Exclude: - - 'spec/controllers/statuses_controller_spec.rb' - 'spec/models/account_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). @@ -637,10 +603,6 @@ RSpec/RepeatedExample: Exclude: - 'spec/policies/status_policy_spec.rb' -RSpec/RepeatedExampleGroupBody: - Exclude: - - 'spec/controllers/statuses_controller_spec.rb' - RSpec/StubbedMock: Exclude: - 'spec/controllers/api/base_controller_spec.rb' diff --git a/Dockerfile b/Dockerfile index 91c26d2ac..cb5096581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,7 +55,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] ENV DEBIAN_FRONTEND="noninteractive" \ PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" -# Ignoreing these here since we don't want to pin any versions and the Debian image removes apt-get content after use +# Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use # hadolint ignore=DL3008,DL3009 RUN apt-get update && \ echo "Etc/UTC" > /etc/localtime && \ diff --git a/Gemfile b/Gemfile index e393b2192..67a0fb078 100644 --- a/Gemfile +++ b/Gemfile @@ -99,54 +99,87 @@ gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' -group :development, :test do - gem 'fabrication', '~> 2.30' - gem 'fuubar', '~> 2.5' - gem 'i18n-tasks', '~> 1.0', require: false +gem 'private_address_check', '~> 0.5' + +group :test do + # RSpec runner for rails gem 'rspec-rails', '~> 6.0' + + # Used to split testing into chunks in CI gem 'rspec_chunked', '~> 0.6' - gem 'rubocop-capybara', require: false - gem 'rubocop-performance', require: false - gem 'rubocop-rails', require: false - gem 'rubocop-rspec', require: false - gem 'rubocop', require: false -end + # RSpec progress bar formatter + gem 'fuubar', '~> 2.5' -group :production, :test do - gem 'private_address_check', '~> 0.5' -end + # Extra RSpec extenion methods and helpers for sidekiq + gem 'rspec-sidekiq', '~> 3.1' -group :test do + # Browser integration testing gem 'capybara', '~> 3.39' - gem 'climate_control' + + # Used to mock environment variables + gem 'climate_control', '~> 0.2' + + # Generating fake data for specs gem 'faker', '~> 3.2' + + # Generate test objects for specs + gem 'fabrication', '~> 2.30' + + # Add back helpers functions removed in Rails 5.1 + gem 'rails-controller-testing', '~> 1.0' + + # Validate schemas in specs gem 'json-schema', '~> 4.0' + + # Test harness fo rack components gem 'rack-test', '~> 2.1' - gem 'rails-controller-testing', '~> 1.0' - gem 'rspec_junit_formatter', '~> 0.6' - gem 'rspec-sidekiq', '~> 3.1' + + # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false gem 'simplecov', '~> 0.22', require: false + + # Stub web requests for specs gem 'webmock', '~> 3.18' end group :development do + # Code linting CLI and plugins + gem 'rubocop', require: false + gem 'rubocop-capybara', require: false + gem 'rubocop-performance', require: false + gem 'rubocop-rails', require: false + gem 'rubocop-rspec', require: false + + # Annotates modules with schema gem 'annotate', '~> 3.2' + + # Enhanced error message pages for development gem 'better_errors', '~> 2.9' gem 'binding_of_caller', '~> 1.0' + + # Preview mail in the browser gem 'letter_opener', '~> 1.8' gem 'letter_opener_web', '~> 2.0' - gem 'memory_profiler' + + # Security analysis CLI tools gem 'brakeman', '~> 5.4', require: false gem 'bundler-audit', '~> 0.9', require: false + + # Linter CLI for HAML files gem 'haml_lint', require: false + # Deployment automation gem 'capistrano', '~> 3.17' gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rbenv', '~> 2.2' gem 'capistrano-yarn', '~> 2.0' - gem 'stackprof' + # Validate missing i18n keys + gem 'i18n-tasks', '~> 1.0', require: false + + # Profiling tools + gem 'memory_profiler', require: false + gem 'stackprof', require: false end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index b5d277097..acea3bbbe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -601,8 +601,6 @@ GEM sidekiq (>= 2.4.0) rspec-support (3.12.0) rspec_chunked (0.6) - rspec_junit_formatter (0.6.0) - rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.50.2) json (~> 2.3) parallel (~> 1.10) @@ -787,7 +785,7 @@ DEPENDENCIES capybara (~> 3.39) charlock_holmes (~> 0.7.7) chewy (~> 7.3) - climate_control + climate_control (~> 0.2) cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby @@ -866,7 +864,6 @@ DEPENDENCIES rspec-rails (~> 6.0) rspec-sidekiq (~> 3.1) rspec_chunked (~> 0.6) - rspec_junit_formatter (~> 0.6) rubocop rubocop-capybara rubocop-performance diff --git a/app/controllers/api/v1/admin/domain_allows_controller.rb b/app/controllers/api/v1/admin/domain_allows_controller.rb index 61e1d481c..dd54d6710 100644 --- a/app/controllers/api/v1/admin/domain_allows_controller.rb +++ b/app/controllers/api/v1/admin/domain_allows_controller.rb @@ -29,7 +29,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController def create authorize :domain_allow, :create? - @domain_allow = DomainAllow.find_by(resource_params) + @domain_allow = DomainAllow.find_by(domain: resource_params[:domain]) if @domain_allow.nil? @domain_allow = DomainAllow.create!(resource_params) diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 1be15a5a4..e3769437b 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -2,6 +2,8 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController include Authorization + include Redisable + include Lockable before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action :require_user! @@ -10,7 +12,9 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController override_rate_limit_headers :create, family: :statuses def create - @status = ReblogService.new.call(current_account, @reblog, reblog_params) + with_redis_lock("reblog:#{current_account.id}:#{@reblog.id}") do + @status = ReblogService.new.call(current_account, @reblog, reblog_params) + end render json: @status, serializer: REST::StatusSerializer end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index a49d23a9f..a9d92b6e2 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -132,7 +132,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_sessions - @sessions = current_user.session_activations + @sessions = current_user.session_activations.order(updated_at: :desc) end def set_strikes diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index efae7e35f..0a1df5506 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -10,6 +10,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :set_body_classes before_action :set_cache_headers + before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json } + skip_before_action :require_functional! include Localized @@ -40,4 +42,14 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio def set_cache_headers response.cache_control.replace(private: true, no_store: true) end + + def set_last_used_at_by_app + @last_used_at_by_app = Doorkeeper::AccessToken + .select('DISTINCT ON (application_id) application_id, last_used_at') + .where(resource_owner_id: current_resource_owner.id) + .where.not(last_used_at: nil) + .order(application_id: :desc, last_used_at: :desc) + .pluck(:application_id, :last_used_at) + .to_h + end end diff --git a/app/javascript/mastodon/utils/__tests__/html-test.js b/app/javascript/mastodon/utils/__tests__/html-test.js index ef9296e6c..d948cf4c5 100644 --- a/app/javascript/mastodon/utils/__tests__/html-test.js +++ b/app/javascript/mastodon/utils/__tests__/html-test.js @@ -1,7 +1,7 @@ import * as html from '../html'; describe('html', () => { - describe('unsecapeHTML', () => { + describe('unescapeHTML', () => { it('returns unescaped HTML', () => { const output = html.unescapeHTML('

lorem

ipsum


<br>'); expect(output).toEqual('lorem\n\nipsum\n
'); diff --git a/app/javascript/types/image.d.ts b/app/javascript/types/image.d.ts index fae2ed701..15f0007af 100644 --- a/app/javascript/types/image.d.ts +++ b/app/javascript/types/image.d.ts @@ -14,11 +14,6 @@ declare module '*.jpg' { export default path; } -declare module '*.jpg' { - const path: string; - export default path; -} - declare module '*.png' { const path: string; export default path; diff --git a/app/lib/activitypub/activity/flag.rb b/app/lib/activitypub/activity/flag.rb index dc808ad36..dc1932f59 100644 --- a/app/lib/activitypub/activity/flag.rb +++ b/app/lib/activitypub/activity/flag.rb @@ -16,7 +16,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity @account, target_account, status_ids: target_statuses.nil? ? [] : target_statuses.map(&:id), - comment: @json['content'] || '', + comment: report_comment, uri: report_uri ) end @@ -35,4 +35,8 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity def report_uri @json['id'] unless @json['id'].nil? || non_matching_uri_hosts?(@account.uri, @json['id']) end + + def report_comment + (@json['content'] || '')[0...5000] + end end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index a65a9565a..864328631 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -28,6 +28,8 @@ class ActivityPub::TagManager return activity_account_status_url(target.account, target) if target.reblog? short_account_status_url(target.account, target) + when :flag + target.uri end end @@ -43,6 +45,8 @@ class ActivityPub::TagManager account_status_url(target.account, target) when :emoji emoji_url(target) + when :flag + target.uri end end diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb index d61ec0e6e..4de69c1ea 100644 --- a/app/lib/application_extension.rb +++ b/app/lib/application_extension.rb @@ -9,10 +9,6 @@ module ApplicationExtension validates :redirect_uri, length: { maximum: 2_000 } end - def most_recently_used_access_token - @most_recently_used_access_token ||= access_tokens.where.not(last_used_at: nil).order(last_used_at: :desc).first - end - def confirmation_redirect_uri redirect_uri.lines.first.strip end diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb index f8a0be636..dfed69285 100644 --- a/app/lib/link_details_extractor.rb +++ b/app/lib/link_details_extractor.rb @@ -140,7 +140,7 @@ class LinkDetailsExtractor end def html - player_url.present? ? content_tag(:iframe, nil, src: player_url, width: width, height: height, allowtransparency: 'true', scrolling: 'no', frameborder: '0') : nil + player_url.present? ? content_tag(:iframe, nil, src: player_url, width: width, height: height, allowfullscreen: 'true', allowtransparency: 'true', scrolling: 'no', frameborder: '0') : nil end def width diff --git a/app/models/report.rb b/app/models/report.rb index c3a0c4c8b..e738281ad 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -40,7 +40,10 @@ class Report < ApplicationRecord scope :resolved, -> { where.not(action_taken_at: nil) } scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with({ user: [:invite_request, :invite] })) } - validates :comment, length: { maximum: 1_000 } + # A report is considered local if the reporter is local + delegate :local?, to: :account + + validates :comment, length: { maximum: 1_000 }, if: :local? validates :rule_ids, absence: true, unless: :violation? validate :validate_rule_ids @@ -51,10 +54,6 @@ class Report < ApplicationRecord violation: 2_000, } - def local? - false # Force uri_for to use uri attribute - end - before_validation :set_uri, only: :create after_create_commit :trigger_webhooks diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 89b8d06cd..5a99eb50c 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -101,8 +101,8 @@ class BackupService < BaseService actor[:likes] = 'likes.json' actor[:bookmarks] = 'bookmarks.json' - download_to_zip(tar, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists? - download_to_zip(tar, account.header, "header#{File.extname(account.header.path)}") if account.header.exists? + download_to_zip(zipfile, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists? + download_to_zip(zipfile, account.header, "header#{File.extname(account.header.path)}") if account.header.exists? json = Oj.dump(actor) diff --git a/app/validators/vote_validator.rb b/app/validators/vote_validator.rb index 9bd17fbe8..fa2bd223d 100644 --- a/app/validators/vote_validator.rb +++ b/app/validators/vote_validator.rb @@ -3,8 +3,8 @@ class VoteValidator < ActiveModel::Validator def validate(vote) vote.errors.add(:base, I18n.t('polls.errors.expired')) if vote.poll_expired? - vote.errors.add(:base, I18n.t('polls.errors.invalid_choice')) if invalid_choice?(vote) + vote.errors.add(:base, I18n.t('polls.errors.self_vote')) if self_vote?(vote) vote.errors.add(:base, I18n.t('polls.errors.already_voted')) if additional_voting_not_allowed?(vote) end @@ -27,6 +27,10 @@ class VoteValidator < ActiveModel::Validator vote.choice.negative? || vote.choice >= vote.poll.options.size end + def self_vote?(vote) + vote.account_id == vote.poll.account_id + end + def already_voted_for_same_choice_on_multiple_poll?(vote) if vote.persisted? account_votes_on_same_poll(vote).where(choice: vote.choice).where.not(poll_votes: { id: vote }).exists? diff --git a/app/views/oauth/authorized_applications/index.html.haml b/app/views/oauth/authorized_applications/index.html.haml index 0280d8aef..55d8524db 100644 --- a/app/views/oauth/authorized_applications/index.html.haml +++ b/app/views/oauth/authorized_applications/index.html.haml @@ -18,8 +18,8 @@ .announcements-list__item__action-bar .announcements-list__item__meta - - if application.most_recently_used_access_token - = t('doorkeeper.authorized_applications.index.last_used_at', date: l(application.most_recently_used_access_token.last_used_at.to_date)) + - if @last_used_at_by_app[application.id] + = t('doorkeeper.authorized_applications.index.last_used_at', date: l(@last_used_at_by_app[application.id].to_date)) - else = t('doorkeeper.authorized_applications.index.never_used') diff --git a/app/workers/post_process_media_worker.rb b/app/workers/post_process_media_worker.rb index 996d5def9..2d11b00c2 100644 --- a/app/workers/post_process_media_worker.rb +++ b/app/workers/post_process_media_worker.rb @@ -24,7 +24,7 @@ class PostProcessMediaWorker media_attachment.processing = :in_progress media_attachment.save - # Because paperclip-av-transcover overwrites this attribute + # Because paperclip-av-transcoder overwrites this attribute # we will save it here and restore it after reprocess is done previous_meta = media_attachment.file_meta diff --git a/config/initializers/ffmpeg.rb b/config/initializers/ffmpeg.rb index 4c0bf779d..cd5914eb5 100644 --- a/config/initializers/ffmpeg.rb +++ b/config/initializers/ffmpeg.rb @@ -1,3 +1,3 @@ if ENV['FFMPEG_BINARY'].present? - FFMPEG.ffmpeg_binary = ENV['FFMPEG_BINARY'] + FFMPEG.ffmpeg_binary = ENV['FFMPEG_BINARY'] end diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index f01670146..c2cd444f0 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -73,7 +73,7 @@ Devise.setup do |config| oidc_options[:display_name] = ENV['OIDC_DISPLAY_NAME'] #OPTIONAL oidc_options[:issuer] = ENV['OIDC_ISSUER'] if ENV['OIDC_ISSUER'] #NEED oidc_options[:discovery] = ENV['OIDC_DISCOVERY'] == 'true' if ENV['OIDC_DISCOVERY'] #OPTIONAL (default: false) - oidc_options[:client_auth_method] = ENV['OIDC_CLIENT_AUTH_METHOD'] if ENV['OIDC_CLIENT_AUTH_METHOD'] #OPTIONAL (default: basic) + oidc_options[:client_auth_method] = ENV['OIDC_CLIENT_AUTH_METHOD'] if ENV['OIDC_CLIENT_AUTH_METHOD'] #OPTIONAL (default: basic) scope_string = ENV['OIDC_SCOPE'] if ENV['OIDC_SCOPE'] #NEED scopes = scope_string.split(',') oidc_options[:scope] = scopes.map { |x| x.to_sym } diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 9f0ffc6dc..093d2ba9a 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -61,13 +61,13 @@ if ENV['S3_ENABLED'] == 'true' s3_options: { signature_version: ENV.fetch('S3_SIGNATURE_VERSION') { 'v4' }, - http_open_timeout: ENV.fetch('S3_OPEN_TIMEOUT'){ '5' }.to_i, - http_read_timeout: ENV.fetch('S3_READ_TIMEOUT'){ '5' }.to_i, + http_open_timeout: ENV.fetch('S3_OPEN_TIMEOUT') { '5' }.to_i, + http_read_timeout: ENV.fetch('S3_READ_TIMEOUT') { '5' }.to_i, http_idle_timeout: 5, retry_limit: 0, } ) - + Paperclip::Attachment.default_options[:s3_permissions] = ->(*) { nil } if ENV['S3_PERMISSION'] == '' if ENV.has_key?('S3_ENDPOINT') @@ -124,7 +124,7 @@ elsif ENV['SWIFT_ENABLED'] == 'true' openstack_cache_ttl: ENV.fetch('SWIFT_CACHE_TTL') { 60 }, openstack_temp_url_key: ENV['SWIFT_TEMP_URL_KEY'], }, - + fog_file: { 'Cache-Control' => 'public, max-age=315576000, immutable' }, fog_directory: ENV['SWIFT_CONTAINER'], diff --git a/config/initializers/webauthn.rb b/config/initializers/webauthn.rb index a0a5b8153..a4f027947 100644 --- a/config/initializers/webauthn.rb +++ b/config/initializers/webauthn.rb @@ -1,7 +1,7 @@ WebAuthn.configure do |config| # This value needs to match `window.location.origin` evaluated by # the User Agent during registration and authentication ceremonies. - config.origin = "#{Rails.configuration.x.use_https ? 'https' : 'http' }://#{Rails.configuration.x.web_domain}" + config.origin = "#{Rails.configuration.x.use_https ? 'https' : 'http'}://#{Rails.configuration.x.web_domain}" # Relying Party name for display purposes config.rp_name = "Mastodon" diff --git a/config/locales/en.yml b/config/locales/en.yml index aea965660..76198763a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1446,6 +1446,7 @@ en: expired: The poll has already ended invalid_choice: The chosen vote option does not exist over_character_limit: cannot be longer than %{max} characters each + self_vote: You cannot vote in your own polls too_few_options: must have more than one item too_many_options: can't contain more than %{max} items preferences: diff --git a/jest.config.js b/jest.config.js index d6e7bc65f..922b0f550 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,7 +10,6 @@ const config = { '/tmp/', '/app/javascript/themes/', ], - setupFiles: ['raf/polyfill'], setupFilesAfterEnv: ['/app/javascript/mastodon/test_setup.js'], collectCoverageFrom: [ 'app/javascript/mastodon/**/*.{js,jsx,ts,tsx}', diff --git a/lib/mastodon/media_cli.rb b/lib/mastodon/media_cli.rb index 7dacd8d3d..1bedcd9be 100644 --- a/lib/mastodon/media_cli.rb +++ b/lib/mastodon/media_cli.rb @@ -24,7 +24,7 @@ module Mastodon desc 'remove', 'Remove remote media files, headers or avatars' long_desc <<-DESC Removes locally cached copies of media attachments (and optionally profile - headers and avatars) from other servers. By default, only media attachements + headers and avatars) from other servers. By default, only media attachments are removed. The --days option specifies how old media attachments have to be before they are removed. In case of avatars and headers, it specifies how old diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake index ceb421f4b..60399c9de 100644 --- a/lib/tasks/tests.rake +++ b/lib/tasks/tests.rake @@ -25,7 +25,7 @@ namespace :tests do end if Account.where(domain: Rails.configuration.x.local_domain).exists? - puts 'Faux remote accounts not properly claned up' + puts 'Faux remote accounts not properly cleaned up' exit(1) end diff --git a/package.json b/package.json index ad98f977d..11831f02d 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "intl-messageformat": "^2.2.0", "intl-relativeformat": "^6.4.3", "js-yaml": "^4.1.0", - "jsdom": "^21.1.2", + "jsdom": "^22.0.0", "lodash": "^4.17.21", "mark-loader": "^0.1.6", "marky": "^1.2.5", @@ -158,7 +158,6 @@ "@types/pg": "^8.6.6", "@types/prop-types": "^15.7.5", "@types/punycode": "^2.1.0", - "@types/raf": "^3.4.0", "@types/react": "^16.14.38", "@types/react-dom": "^16.9.18", "@types/react-helmet": "^6.1.6", @@ -198,7 +197,6 @@ "jest-environment-jsdom": "^29.5.0", "lint-staged": "^13.2.2", "prettier": "^2.8.8", - "raf": "^3.4.1", "react-intl-translations-manager": "^5.0.3", "react-test-renderer": "^16.14.0", "stylelint": "^15.6.1", diff --git a/spec/controllers/admin/confirmations_controller_spec.rb b/spec/controllers/admin/confirmations_controller_spec.rb index ffab56d9a..181616a66 100644 --- a/spec/controllers/admin/confirmations_controller_spec.rb +++ b/spec/controllers/admin/confirmations_controller_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Admin::ConfirmationsController do end end - describe 'POST #resernd' do + describe 'POST #resend' do subject { post :resend, params: { account_id: user.account.id } } let!(:user) { Fabricate(:user, confirmed_at: confirmed_at) } diff --git a/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb index e5ee28882..fe39596df 100644 --- a/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb +++ b/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb @@ -5,23 +5,182 @@ require 'rails_helper' describe Api::V1::Admin::CanonicalEmailBlocksController do render_views - let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } - let(:account) { Fabricate(:account) } + 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, params: { account_id: account.id, limit: 2 } + 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 @@ -68,4 +227,132 @@ describe Api::V1::Admin::CanonicalEmailBlocksController do 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 diff --git a/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb b/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb index 9db8a35b4..ca63ea5a7 100644 --- a/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb +++ b/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb @@ -128,5 +128,13 @@ RSpec.describe Api::V1::Admin::DomainAllowsController 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 diff --git a/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb index a92a29869..3643eb0f3 100644 --- a/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb +++ b/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb @@ -5,19 +5,280 @@ require 'rails_helper' describe Api::V1::Admin::EmailDomainBlocksController do render_views - let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } + 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(:account) { Fabricate(:account) } + let(:scopes) { 'admin:read:email_domain_blocks admin:write:email_domain_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, params: { account_id: account.id, limit: 2 } + get :index expect(response).to have_http_status(200) end + + context 'when there is no email domain block' do + it 'returns an empty list' do + get :index + + json = body_as_json + + expect(json).to be_empty + end + end + + context 'when there are email domain blocks' do + let!(:email_domain_blocks) { Fabricate.times(5, :email_domain_block) } + let(:blocked_email_domains) { email_domain_blocks.pluck(:domain) } + + it 'return the correct blocked email domains' do + get :index + + json = body_as_json + + expect(json.pluck(:domain)).to match_array(blocked_email_domains) + end + + context 'with limit param' do + let(:params) { { limit: 2 } } + + it 'returns only the requested number of email domain 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: email_domain_blocks[1].id } } + + it 'returns only the email domain blocks after since_id' do + get :index, params: params + + email_domain_blocks_ids = email_domain_blocks.pluck(:id).map(&:to_s) + json = body_as_json + + expect(json.pluck(:id)).to match_array(email_domain_blocks_ids[2..]) + end + end + + context 'with max_id param' do + let(:params) { { max_id: email_domain_blocks[3].id } } + + it 'returns only the email domain blocks before max_id' do + get :index, params: params + + email_domain_blocks_ids = email_domain_blocks.pluck(:id).map(&:to_s) + json = body_as_json + + expect(json.pluck(:id)).to match_array(email_domain_blocks_ids[..2]) + end + end + end + end + + describe 'GET #show' do + let!(:email_domain_block) { Fabricate(:email_domain_block) } + let(:params) { { id: email_domain_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 email domain block exists' do + it 'returns http success' do + get :show, params: params + + expect(response).to have_http_status(200) + end + + it 'returns the correct blocked domain' do + get :show, params: params + + json = body_as_json + + expect(json[:domain]).to eq(email_domain_block.domain) + end + end + + context 'when email domain 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) { { domain: 'example.com' } } + + 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 the correct blocked email domain' do + post :create, params: params + + json = body_as_json + + expect(json[:domain]).to eq(params[:domain]) + end + + context 'when domain param is not provided' do + let(:params) { { domain: '' } } + + it 'returns http unprocessable entity' do + post :create, params: params + + expect(response).to have_http_status(422) + end + end + + context 'when provided domain name has an invalid character' do + let(:params) { { domain: 'do\uD800.com' } } + + it 'returns http unprocessable entity' do + post :create, params: params + + expect(response).to have_http_status(422) + end + end + + context 'when provided domain is already blocked' do + before do + EmailDomainBlock.create(params) + 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!(:email_domain_block) { Fabricate(:email_domain_block) } + let(:params) { { id: email_domain_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 + + it 'returns an empty body' do + delete :destroy, params: params + + json = body_as_json + + expect(json).to be_empty + end + + it 'deletes email domain block' do + delete :destroy, params: params + + email_domain_block = EmailDomainBlock.find_by(id: params[:id]) + + expect(email_domain_block).to be_nil + end + + context 'when email domain 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 diff --git a/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb index 50e2ae968..a5787883e 100644 --- a/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb +++ b/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb @@ -5,19 +5,305 @@ require 'rails_helper' describe Api::V1::Admin::IpBlocksController do render_views - let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } - let(:account) { Fabricate(:account) } + 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 - get :index, params: { account_id: account.id, limit: 2 } + 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 diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb index df20a5d7e..e6b7f1849 100644 --- a/spec/controllers/concerns/signature_verification_spec.rb +++ b/spec/controllers/concerns/signature_verification_spec.rb @@ -129,7 +129,7 @@ describe ApplicationController do end end - context 'with request with unparseable Date header' do + context 'with request with unparsable Date header' do before do get :success diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index c846dd1d6..1885814cd 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -719,65 +719,180 @@ describe StatusesController do end context 'when status is public' do - pending + before do + status.update(visibility: :public) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end end context 'when status is private' do - pending + before do + status.update(visibility: :private) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http not_found' do + expect(response).to have_http_status(404) + end end context 'when status is direct' do - pending + before do + status.update(visibility: :direct) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http not_found' do + expect(response).to have_http_status(404) + end end context 'when signed-in' do + let(:user) { Fabricate(:user) } + + before do + sign_in(user) + end + context 'when status is public' do - pending + before do + status.update(visibility: :public) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end end context 'when status is private' do + before do + status.update(visibility: :private) + end + context 'when user is authorized to see it' do - pending + before do + user.account.follow!(account) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end end context 'when user is not authorized to see it' do - pending + before do + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http not_found' do + expect(response).to have_http_status(404) + end end end context 'when status is direct' do + before do + status.update(visibility: :direct) + end + context 'when user is authorized to see it' do - pending + before do + Fabricate(:mention, account: user.account, status: status) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end end context 'when user is not authorized to see it' do - pending + before do + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http not_found' do + expect(response).to have_http_status(404) + end end end end context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com') } + + before do + allow(controller).to receive(:signed_request_actor).and_return(remote_account) + end + context 'when status is public' do - pending + before do + status.update(visibility: :public) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end end context 'when status is private' do + before do + status.update(visibility: :private) + end + context 'when user is authorized to see it' do - pending + before do + remote_account.follow!(account) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end end context 'when user is not authorized to see it' do - pending + before do + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http not_found' do + expect(response).to have_http_status(404) + end end end context 'when status is direct' do + before do + status.update(visibility: :direct) + end + context 'when user is authorized to see it' do - pending + before do + Fabricate(:mention, account: remote_account, status: status) + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end end context 'when user is not authorized to see it' do - pending + before do + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http not_found' do + expect(response).to have_http_status(404) + end end end end diff --git a/spec/fabricators/canonical_email_block_fabricator.rb b/spec/fabricators/canonical_email_block_fabricator.rb index 21d7c2402..3a018059f 100644 --- a/spec/fabricators/canonical_email_block_fabricator.rb +++ b/spec/fabricators/canonical_email_block_fabricator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true Fabricator(:canonical_email_block) do - email 'test@example.com' + email { sequence(:email) { |i| "#{i}#{Faker::Internet.email}" } } reference_account { Fabricate(:account) } end diff --git a/spec/lib/activitypub/activity/flag_spec.rb b/spec/lib/activitypub/activity/flag_spec.rb index 005e185e6..601473069 100644 --- a/spec/lib/activitypub/activity/flag_spec.rb +++ b/spec/lib/activitypub/activity/flag_spec.rb @@ -39,6 +39,37 @@ RSpec.describe ActivityPub::Activity::Flag do end end + context 'when the report comment is excessively long' do + subject do + described_class.new({ + '@context': 'https://www.w3.org/ns/activitystreams', + id: flag_id, + type: 'Flag', + content: long_comment, + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: [ + ActivityPub::TagManager.instance.uri_for(flagged), + ActivityPub::TagManager.instance.uri_for(status), + ], + }.with_indifferent_access, sender) + end + + let(:long_comment) { Faker::Lorem.characters(number: 6000) } + + before do + subject.perform + end + + it 'creates a report but with a truncated comment' do + report = Report.find_by(account: sender, target_account: flagged) + + expect(report).to_not be_nil + expect(report.comment.length).to eq 5000 + expect(report.comment).to eq long_comment[0...5000] + expect(report.status_ids).to eq [status.id] + end + end + context 'when the reported status is private and should not be visible to the remote server' do let(:status) { Fabricate(:status, account: flagged, uri: 'foobar', visibility: :private) } diff --git a/spec/lib/mastodon/migration_warning_spec.rb b/spec/lib/mastodon/migration_warning_spec.rb new file mode 100644 index 000000000..4adf0837a --- /dev/null +++ b/spec/lib/mastodon/migration_warning_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'mastodon/migration_warning' + +describe Mastodon::MigrationWarning do + describe 'migration_duration_warning' do + before do + allow(migration).to receive(:valid_environment?).and_return(true) + allow(migration).to receive(:sleep).with(1) + end + + let(:migration) { Class.new(ActiveRecord::Migration[6.1]).extend(described_class) } + + context 'with the default message' do + it 'warns about long migrations' do + expectation = expect { migration.migration_duration_warning } + + expectation.to output(/interrupt this migration/).to_stdout + expectation.to output(/Continuing in 5/).to_stdout + end + end + + context 'with an additional message' do + it 'warns about long migrations' do + expectation = expect { migration.migration_duration_warning('Get ready for it') } + + expectation.to output(/interrupt this migration/).to_stdout + expectation.to output(/Get ready for it/).to_stdout + expectation.to output(/Continuing in 5/).to_stdout + end + end + end +end diff --git a/spec/models/account_migration_spec.rb b/spec/models/account_migration_spec.rb index 0d97ea7e7..519b9a97a 100644 --- a/spec/models/account_migration_spec.rb +++ b/spec/models/account_migration_spec.rb @@ -25,7 +25,7 @@ RSpec.describe AccountMigration do end end - context 'with unresolveable account' do + context 'with unresolvable account' do let(:target_acct) { 'target@remote' } before do diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index bbe35f579..6e9c608ab 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -698,7 +698,7 @@ RSpec.describe Account do expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil end - xit 'does not match URL querystring' do + xit 'does not match URL query string' do expect(subject.match('https://example.com/?x=@alice')).to be_nil end end diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index b006f60bb..0093dcd8d 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -121,10 +121,17 @@ describe Report do end describe 'validations' do - it 'is invalid if comment is longer than 1000 characters' do + let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } + + it 'is invalid if comment is longer than 1000 characters only if reporter is local' do report = Fabricate.build(:report, comment: Faker::Lorem.characters(number: 1001)) - report.valid? + expect(report.valid?).to be false expect(report).to model_have_error_on_field(:comment) end + + it 'is valid if comment is longer than 1000 characters and reporter is not local' do + report = Fabricate.build(:report, account: remote_account, comment: Faker::Lorem.characters(number: 1001)) + expect(report.valid?).to be true + end end end diff --git a/spec/models/user_settings/setting_spec.rb b/spec/models/user_settings/setting_spec.rb index 9884ae4f8..8c8d31ec5 100644 --- a/spec/models/user_settings/setting_spec.rb +++ b/spec/models/user_settings/setting_spec.rb @@ -90,7 +90,7 @@ RSpec.describe UserSettings::Setting do describe '#key' do context 'when there is no namespace' do - it 'returnsn a symbol' do + it 'returns a symbol' do expect(subject.key).to eq :foo end end diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb index 4c5e6b6cc..db454d7ad 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -181,7 +181,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do '@context': ['https://www.w3.org/ns/activitystreams'], id: "https://foo.test/users/#{i}/featured", type: 'OrderedCollection', - totelItems: 1, + totalItems: 1, orderedItems: [status_json], }.with_indifferent_access webfinger = { diff --git a/spec/services/backup_service_spec.rb b/spec/services/backup_service_spec.rb index b961b7f67..73e0b42ad 100644 --- a/spec/services/backup_service_spec.rb +++ b/spec/services/backup_service_spec.rb @@ -21,6 +21,27 @@ RSpec.describe BackupService, type: :service do end end + context 'when the user has an avatar and header' do + before do + user.account.update!(avatar: attachment_fixture('avatar.gif')) + user.account.update!(header: attachment_fixture('emojo.png')) + end + + it 'stores them as expected' do + service_call + + json = Oj.load(read_zip_file(backup, 'actor.json')) + avatar_path = json.dig('icon', 'url') + header_path = json.dig('image', 'url') + + expect(avatar_path).to_not be_nil + expect(header_path).to_not be_nil + + expect(read_zip_file(backup, avatar_path)).to be_present + expect(read_zip_file(backup, header_path)).to be_present + end + end + it 'marks the backup as processed' do expect { service_call }.to change(backup, :processed).from(false).to(true) end diff --git a/spec/services/report_service_spec.rb b/spec/services/report_service_spec.rb index 29207462a..b8ceedb85 100644 --- a/spec/services/report_service_spec.rb +++ b/spec/services/report_service_spec.rb @@ -6,6 +6,14 @@ RSpec.describe ReportService, type: :service do subject { described_class.new } let(:source_account) { Fabricate(:account) } + let(:target_account) { Fabricate(:account) } + + context 'with a local account' do + it 'has a uri' do + report = subject.call(source_account, target_account) + expect(report.uri).to_not be_nil + end + end context 'with a remote account' do let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } @@ -35,7 +43,6 @@ RSpec.describe ReportService, type: :service do -> { described_class.new.call(source_account, target_account, status_ids: [status.id]) } end - let(:target_account) { Fabricate(:account) } let(:status) { Fabricate(:status, account: target_account, visibility: :direct) } context 'when it is addressed to the reporter' do @@ -91,8 +98,7 @@ RSpec.describe ReportService, type: :service do -> { described_class.new.call(source_account, target_account) } end - let!(:target_account) { Fabricate(:account) } - let!(:other_report) { Fabricate(:report, target_account: target_account) } + let!(:other_report) { Fabricate(:report, target_account: target_account) } before do ActionMailer::Base.deliveries.clear diff --git a/yarn.lock b/yarn.lock index f22002c13..c77038f6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2184,11 +2184,6 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== -"@types/raf@^3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2" - integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw== - "@types/range-parser@*": version "1.2.4" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" @@ -2800,7 +2795,7 @@ acorn@^6.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -acorn@^8.0.4, acorn@^8.1.0, acorn@^8.5.0, acorn@^8.8.0, acorn@^8.8.1, acorn@^8.8.2: +acorn@^8.0.4, acorn@^8.1.0, acorn@^8.5.0, acorn@^8.8.0, acorn@^8.8.1: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== @@ -7537,19 +7532,16 @@ jsdom@^20.0.0: ws "^8.11.0" xml-name-validator "^4.0.0" -jsdom@^21.1.2: - version "21.1.2" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.1.2.tgz#6433f751b8718248d646af1cdf6662dc8a1ca7f9" - integrity sha512-sCpFmK2jv+1sjff4u7fzft+pUh2KSUbUrEHYHyfSIbGTIcmnjyp83qg6qLwdJ/I3LpTXx33ACxeRL7Lsyc6lGQ== +jsdom@^22.0.0: + version "22.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-22.0.0.tgz#3295c6992c70089c4b8f5cf060489fddf7ee9816" + integrity sha512-p5ZTEb5h+O+iU02t0GfEjAnkdYPrQSkfuTSMkMYyIoMvUNEHsbG0bHHbfXIcfTqD2UfvjQX7mmgiFsyRwGscVw== dependencies: abab "^2.0.6" - acorn "^8.8.2" - acorn-globals "^7.0.0" cssstyle "^3.0.0" data-urls "^4.0.0" decimal.js "^10.4.3" domexception "^4.0.0" - escodegen "^2.0.0" form-data "^4.0.0" html-encoding-sniffer "^3.0.0" http-proxy-agent "^5.0.0" @@ -9520,7 +9512,7 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== -raf@^3.1.0, raf@^3.4.1: +raf@^3.1.0: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==