diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 97a363d1e6..da4203e357 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -43,9 +43,16 @@ jobs: type=edge,branch=main type=sha,prefix=,format=long + - name: Generate version suffix + id: version_vars + if: github.repository == 'mastodon/mastodon' && github.event_name == 'push' && github.ref_name == 'main' + run: | + echo mastodon_version_suffix=+edge-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT + - uses: docker/build-push-action@v4 with: context: . + build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }} platforms: linux/amd64,linux/arm64 provenance: false builder: ${{ steps.buildx.outputs.name }} diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml index 501db6e9c6..f07f7447ca 100644 --- a/.github/workflows/build-nightly.yml +++ b/.github/workflows/build-nightly.yml @@ -41,9 +41,15 @@ jobs: labels: | org.opencontainers.image.description=Nightly build image used for testing purposes + - name: Generate version suffix + id: version_vars + run: | + echo mastodon_version_suffix=+nightly-$(date +'%Y%m%d') >> $GITHUB_OUTPUT + - uses: docker/build-push-action@v4 with: context: . + build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }} platforms: linux/amd64,linux/arm64 provenance: false builder: ${{ steps.buildx.outputs.name }} diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml index e0c309c736..7700e48512 100644 --- a/.github/workflows/lint-js.yml +++ b/.github/workflows/lint-js.yml @@ -48,7 +48,7 @@ jobs: run: yarn --frozen-lockfile - name: ESLint - run: yarn test:lint:js + run: yarn test:lint:js --max-warnings 0 - name: Typecheck run: yarn test:typecheck diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index 6b8d6fdfcd..f284745ea4 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -9,7 +9,6 @@ on: env: BUNDLE_CLEAN: true BUNDLE_FROZEN: true - BUNDLE_WITHOUT: 'development production' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -19,8 +18,17 @@ jobs: build: runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + mode: + - production + - test env: - RAILS_ENV: test + RAILS_ENV: ${{ matrix.mode }} + BUNDLE_WITH: ${{ matrix.mode }} + OTP_SECRET: precompile_placeholder + SECRET_KEY_BASE: precompile_placeholder steps: - uses: actions/checkout@v3 @@ -50,6 +58,7 @@ jobs: ./bin/rails assets:precompile - uses: actions/upload-artifact@v3 + if: matrix.mode == 'test' with: path: |- ./public/assets @@ -97,7 +106,7 @@ jobs: PAM_ENABLED: true PAM_DEFAULT_SERVICE: pam_test PAM_CONTROLLED_SERVICE: pam_test_controlled - BUNDLE_WITH: 'pam_authentication' + BUNDLE_WITH: 'pam_authentication test' CI_JOBS: ${{ matrix.ci_job }}/4 strategy: diff --git a/.profile b/.profile index c6d57b609d..f4826ea303 100644 --- a/.profile +++ b/.profile @@ -1 +1 @@ -LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio +LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio:/app/.apt/usr/lib/x86_64-linux-gnu/openblas-pthread diff --git a/.rubocop.yml b/.rubocop.yml index b510c43031..966a2a43db 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -65,6 +65,7 @@ Metrics/AbcSize: Metrics/BlockLength: CountAsOne: ['array', 'hash', 'heredoc', 'method_call'] Exclude: + - 'config/routes.rb' - 'lib/mastodon/*_cli.rb' - 'lib/tasks/*.rake' - 'app/models/concerns/account_associations.rb' @@ -85,6 +86,7 @@ Metrics/BlockLength: - 'config/initializers/simple_form.rb' - 'config/navigation.rb' - 'config/routes.rb' + - 'config/routes/*.rb' - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb' - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb' - 'lib/paperclip/gif_transcoder.rb' @@ -130,6 +132,7 @@ Metrics/ClassLength: - 'app/services/activitypub/process_account_service.rb' - 'app/services/activitypub/process_status_update_service.rb' - 'app/services/backup_service.rb' + - 'app/services/bulk_import_service.rb' - 'app/services/delete_account_service.rb' - 'app/services/fan_out_on_write_service.rb' - 'app/services/fetch_link_card_service.rb' @@ -158,6 +161,11 @@ Metrics/MethodLength: Metrics/ModuleLength: CountAsOne: [array, heredoc] +# Reason: Prevailing style is argument file paths +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath +Rails/FilePath: + EnforcedStyle: arguments + # Reason: Prevailing style uses numeric status codes, matches RSpec/Rails/HttpStatus # https://docs.rubocop.org/rubocop-rails/cops_rails.html#railshttpstatus Rails/HttpStatus: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 58e82a90c7..df1b4718ed 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -21,13 +21,6 @@ Layout/ArgumentAlignment: - 'config/initializers/cors.rb' - 'config/initializers/session_store.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, no_empty_lines -Layout/EmptyLinesAroundBlockBody: - Exclude: - - 'config/routes.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. Layout/ExtraSpacing: @@ -106,28 +99,6 @@ Lint/AmbiguousOperatorPrecedence: Exclude: - 'config/initializers/rack_attack.rb' -# Configuration parameters: AllowedMethods. -# AllowedMethods: enums -Lint/ConstantDefinitionInBlock: - Exclude: - - 'spec/controllers/api/base_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - - 'spec/controllers/concerns/accountable_concern_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - - 'spec/lib/activitypub/adapter_spec.rb' - - 'spec/lib/connection_pool/shared_connection_pool_spec.rb' - - 'spec/lib/connection_pool/shared_timed_stack_spec.rb' - - 'spec/models/concerns/remotable_spec.rb' - -# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. -Lint/DuplicateBranch: - Exclude: - - 'app/lib/permalink_redirector.rb' - - 'app/models/account_statuses_filter.rb' - - 'app/validators/email_mx_validator.rb' - - 'app/validators/vote_validator.rb' - - 'lib/mastodon/maintenance_cli.rb' - # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: Exclude: @@ -168,11 +139,6 @@ Lint/EmptyBlock: - 'spec/models/user_role_spec.rb' - 'spec/models/web/setting_spec.rb' -# Configuration parameters: AllowComments. -Lint/EmptyClass: - Exclude: - - 'spec/controllers/api/base_controller_spec.rb' - Lint/NonLocalExitFromIterator: Exclude: - 'app/helpers/jsonld_helper.rb' @@ -228,6 +194,12 @@ Metrics/AbcSize: Exclude: - 'app/serializers/initial_state_serializer.rb' +# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. +# AllowedMethods: refine +Metrics/BlockLength: + Exclude: + - 'app/models/concerns/status_safe_reblog_insert.rb' + # Configuration parameters: CountBlocks, Max. Metrics/BlockNesting: Exclude: @@ -305,42 +277,6 @@ Naming/VariableNumber: - 'spec/models/user_spec.rb' - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb' -# Configuration parameters: MinSize. -Performance/CollectionLiteralInLoop: - Exclude: - - 'app/models/admin/appeal_filter.rb' - - 'app/models/admin/status_filter.rb' - - 'app/models/relationship_filter.rb' - - 'app/models/trends/preview_card_filter.rb' - - 'app/models/trends/status_filter.rb' - - 'app/presenters/status_relationships_presenter.rb' - - 'app/services/fetch_resource_service.rb' - - 'app/services/suspend_account_service.rb' - - 'app/services/unsuspend_account_service.rb' - - 'config/deploy.rb' - - 'lib/mastodon/media_cli.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/Count: - Exclude: - - 'app/lib/importer/accounts_index_importer.rb' - - 'app/lib/importer/tags_index_importer.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SafeMultiline. -Performance/DeletePrefix: - Exclude: - - 'app/controllers/authorize_interactions_controller.rb' - - 'app/controllers/concerns/signature_verification.rb' - - 'app/controllers/intents_controller.rb' - - 'app/lib/activitypub/case_transform.rb' - - 'app/lib/permalink_redirector.rb' - - 'app/lib/webfinger_resource.rb' - - 'app/services/activitypub/fetch_remote_actor_service.rb' - - 'app/services/backup_service.rb' - - 'app/services/resolve_account_service.rb' - - 'app/services/tag_search_service.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Performance/MapCompact: Exclude: @@ -360,46 +296,12 @@ Performance/MapCompact: - 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb' - 'spec/presenters/status_relationships_presenter_spec.rb' -Performance/MethodObjectAsBlock: - Exclude: - - 'app/models/account_suggestions/source.rb' - - 'spec/models/export_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowRegexpMatch. -Performance/RedundantEqualityComparisonBlock: - Exclude: - - 'spec/requests/link_headers_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: MaxKeyValuePairs. -Performance/RedundantMerge: - Exclude: - - 'config/initializers/paperclip.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). -# Configuration parameters: OnlySumOrWithInitialValue. -Performance/Sum: - Exclude: - - 'app/lib/activity_tracker.rb' - - 'app/models/trends/history.rb' - - 'lib/paperclip/color_extractor.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/TimesMap: - Exclude: - - 'spec/controllers/api/v1/blocks_controller_spec.rb' - - 'spec/controllers/api/v1/mutes_controller_spec.rb' - - 'spec/lib/feed_manager_spec.rb' - - 'spec/lib/request_pool_spec.rb' - - 'spec/models/account_spec.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Performance/UnfreezeString: Exclude: @@ -428,116 +330,6 @@ RSpec/AnyInstance: - 'spec/workers/activitypub/delivery_worker_spec.rb' - 'spec/workers/web/push_notification_worker_spec.rb' -# Configuration parameters: Prefixes, AllowedPatterns. -# Prefixes: when, with, without -RSpec/ContextWording: - Exclude: - - 'spec/config/initializers/rack_attack_spec.rb' - - 'spec/controllers/accounts_controller_spec.rb' - - 'spec/controllers/activitypub/collections_controller_spec.rb' - - 'spec/controllers/activitypub/inboxes_controller_spec.rb' - - 'spec/controllers/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/admin/reports/actions_controller_spec.rb' - - 'spec/controllers/admin/statuses_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/relationships_controller_spec.rb' - - 'spec/controllers/api/v1/accounts_controller_spec.rb' - - 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/api/v1/emails/confirmations_controller_spec.rb' - - 'spec/controllers/api/v1/instances/activity_controller_spec.rb' - - 'spec/controllers/api/v1/instances/peers_controller_spec.rb' - - 'spec/controllers/api/v1/media_controller_spec.rb' - - 'spec/controllers/api/v2/filters_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - - 'spec/controllers/auth/registrations_controller_spec.rb' - - 'spec/controllers/auth/sessions_controller_spec.rb' - - 'spec/controllers/concerns/cache_concern_spec.rb' - - 'spec/controllers/concerns/challengable_concern_spec.rb' - - 'spec/controllers/concerns/localized_spec.rb' - - 'spec/controllers/concerns/rate_limit_headers_spec.rb' - - 'spec/controllers/instance_actors_controller_spec.rb' - - 'spec/controllers/settings/applications_controller_spec.rb' - - 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb' - - 'spec/controllers/statuses_controller_spec.rb' - - 'spec/helpers/admin/account_moderation_notes_helper_spec.rb' - - 'spec/helpers/jsonld_helper_spec.rb' - - 'spec/helpers/routing_helper_spec.rb' - - 'spec/lib/activitypub/activity/accept_spec.rb' - - 'spec/lib/activitypub/activity/announce_spec.rb' - - 'spec/lib/activitypub/activity/create_spec.rb' - - 'spec/lib/activitypub/activity/follow_spec.rb' - - 'spec/lib/activitypub/activity/reject_spec.rb' - - 'spec/lib/advanced_text_formatter_spec.rb' - - 'spec/lib/emoji_formatter_spec.rb' - - 'spec/lib/entity_cache_spec.rb' - - 'spec/lib/feed_manager_spec.rb' - - 'spec/lib/html_aware_formatter_spec.rb' - - 'spec/lib/link_details_extractor_spec.rb' - - 'spec/lib/ostatus/tag_manager_spec.rb' - - 'spec/lib/scope_transformer_spec.rb' - - 'spec/lib/status_cache_hydrator_spec.rb' - - 'spec/lib/status_reach_finder_spec.rb' - - 'spec/lib/text_formatter_spec.rb' - - 'spec/models/account/field_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/admin/account_action_spec.rb' - - 'spec/models/concerns/account_interactions_spec.rb' - - 'spec/models/concerns/remotable_spec.rb' - - 'spec/models/custom_emoji_filter_spec.rb' - - 'spec/models/custom_emoji_spec.rb' - - 'spec/models/email_domain_block_spec.rb' - - 'spec/models/media_attachment_spec.rb' - - 'spec/models/notification_spec.rb' - - 'spec/models/remote_follow_spec.rb' - - 'spec/models/report_spec.rb' - - 'spec/models/session_activation_spec.rb' - - 'spec/models/setting_spec.rb' - - 'spec/models/status_spec.rb' - - 'spec/models/web/push_subscription_spec.rb' - - 'spec/policies/account_moderation_note_policy_spec.rb' - - 'spec/policies/account_policy_spec.rb' - - 'spec/policies/backup_policy_spec.rb' - - 'spec/policies/custom_emoji_policy_spec.rb' - - 'spec/policies/domain_block_policy_spec.rb' - - 'spec/policies/email_domain_block_policy_spec.rb' - - 'spec/policies/instance_policy_spec.rb' - - 'spec/policies/invite_policy_spec.rb' - - 'spec/policies/relay_policy_spec.rb' - - 'spec/policies/report_note_policy_spec.rb' - - 'spec/policies/report_policy_spec.rb' - - 'spec/policies/settings_policy_spec.rb' - - 'spec/policies/tag_policy_spec.rb' - - 'spec/policies/user_policy_spec.rb' - - 'spec/presenters/account_relationships_presenter_spec.rb' - - 'spec/presenters/status_relationships_presenter_spec.rb' - - 'spec/services/account_search_service_spec.rb' - - 'spec/services/account_statuses_cleanup_service_spec.rb' - - 'spec/services/activitypub/fetch_remote_status_service_spec.rb' - - 'spec/services/activitypub/process_account_service_spec.rb' - - 'spec/services/activitypub/process_status_update_service_spec.rb' - - 'spec/services/fetch_link_card_service_spec.rb' - - 'spec/services/fetch_oembed_service_spec.rb' - - 'spec/services/fetch_remote_status_service_spec.rb' - - 'spec/services/follow_service_spec.rb' - - 'spec/services/import_service_spec.rb' - - 'spec/services/notify_service_spec.rb' - - 'spec/services/process_mentions_service_spec.rb' - - 'spec/services/reblog_service_spec.rb' - - 'spec/services/report_service_spec.rb' - - 'spec/services/resolve_account_service_spec.rb' - - 'spec/services/resolve_url_service_spec.rb' - - 'spec/services/search_service_spec.rb' - - 'spec/services/unallow_domain_service_spec.rb' - - 'spec/services/verify_link_service_spec.rb' - - 'spec/validators/disallowed_hashtags_validator_spec.rb' - - 'spec/validators/email_mx_validator_spec.rb' - - 'spec/validators/follow_limit_validator_spec.rb' - - 'spec/validators/poll_validator_spec.rb' - - 'spec/validators/status_pin_validator_spec.rb' - - 'spec/validators/unreserved_username_validator_spec.rb' - - 'spec/validators/url_validator_spec.rb' - - 'spec/workers/move_worker_spec.rb' - - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SkipBlocks, EnforcedStyle. # SupportedStyles: described_class, explicit @@ -701,7 +493,6 @@ RSpec/InstanceVariable: - 'spec/controllers/statuses_cleanup_controller_spec.rb' - 'spec/models/concerns/account_finder_concern_spec.rb' - 'spec/models/concerns/account_interactions_spec.rb' - - 'spec/models/concerns/remotable_spec.rb' - 'spec/models/public_feed_spec.rb' - 'spec/serializers/activitypub/note_serializer_spec.rb' - 'spec/serializers/activitypub/update_poll_serializer_spec.rb' @@ -709,17 +500,6 @@ RSpec/InstanceVariable: - 'spec/services/search_service_spec.rb' - 'spec/services/unblock_domain_service_spec.rb' -RSpec/LeakyConstantDeclaration: - Exclude: - - 'spec/controllers/api/base_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - - 'spec/controllers/concerns/accountable_concern_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - - 'spec/lib/activitypub/adapter_spec.rb' - - 'spec/lib/connection_pool/shared_connection_pool_spec.rb' - - 'spec/lib/connection_pool/shared_timed_stack_spec.rb' - - 'spec/models/concerns/remotable_spec.rb' - RSpec/LetSetup: Exclude: - 'spec/controllers/admin/accounts_controller_spec.rb' @@ -745,6 +525,7 @@ RSpec/LetSetup: - 'spec/controllers/following_accounts_controller_spec.rb' - 'spec/controllers/oauth/authorized_applications_controller_spec.rb' - 'spec/controllers/oauth/tokens_controller_spec.rb' + - 'spec/controllers/settings/imports_controller_spec.rb' - 'spec/lib/activitypub/activity/delete_spec.rb' - 'spec/lib/vacuum/preview_cards_vacuum_spec.rb' - 'spec/models/account_spec.rb' @@ -759,6 +540,7 @@ RSpec/LetSetup: - 'spec/services/activitypub/process_collection_service_spec.rb' - 'spec/services/batched_remove_status_service_spec.rb' - 'spec/services/block_domain_service_spec.rb' + - 'spec/services/bulk_import_service_spec.rb' - 'spec/services/delete_account_service_spec.rb' - 'spec/services/import_service_spec.rb' - 'spec/services/notify_service_spec.rb' @@ -831,17 +613,6 @@ RSpec/MultipleExpectations: RSpec/MultipleMemoizedHelpers: Max: 21 -# This cop supports safe autocorrection (--autocorrect). -RSpec/MultipleSubjects: - Exclude: - - 'spec/controllers/activitypub/collections_controller_spec.rb' - - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb' - - 'spec/controllers/activitypub/outboxes_controller_spec.rb' - - 'spec/controllers/api/web/embeds_controller_spec.rb' - - 'spec/controllers/emojis_controller_spec.rb' - - 'spec/controllers/follower_accounts_controller_spec.rb' - - 'spec/controllers/following_accounts_controller_spec.rb' - # Configuration parameters: AllowedGroups. RSpec/NestedGroups: Max: 6 @@ -867,181 +638,6 @@ RSpec/PredicateMatcher: - 'spec/models/user_spec.rb' - 'spec/services/post_status_service_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Inferences. -RSpec/Rails/InferredSpecType: - Exclude: - - 'spec/controllers/about_controller_spec.rb' - - 'spec/controllers/accounts_controller_spec.rb' - - 'spec/controllers/activitypub/collections_controller_spec.rb' - - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb' - - 'spec/controllers/activitypub/inboxes_controller_spec.rb' - - 'spec/controllers/activitypub/outboxes_controller_spec.rb' - - 'spec/controllers/activitypub/replies_controller_spec.rb' - - 'spec/controllers/admin/account_moderation_notes_controller_spec.rb' - - 'spec/controllers/admin/accounts_controller_spec.rb' - - 'spec/controllers/admin/action_logs_controller_spec.rb' - - 'spec/controllers/admin/base_controller_spec.rb' - - 'spec/controllers/admin/change_emails_controller_spec.rb' - - 'spec/controllers/admin/confirmations_controller_spec.rb' - - 'spec/controllers/admin/dashboard_controller_spec.rb' - - 'spec/controllers/admin/disputes/appeals_controller_spec.rb' - - 'spec/controllers/admin/domain_allows_controller_spec.rb' - - 'spec/controllers/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/admin/email_domain_blocks_controller_spec.rb' - - 'spec/controllers/admin/export_domain_allows_controller_spec.rb' - - 'spec/controllers/admin/export_domain_blocks_controller_spec.rb' - - 'spec/controllers/admin/instances_controller_spec.rb' - - 'spec/controllers/admin/settings/branding_controller_spec.rb' - - 'spec/controllers/admin/tags_controller_spec.rb' - - 'spec/controllers/api/oembed_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/pins_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/search_controller_spec.rb' - - 'spec/controllers/api/v1/accounts_controller_spec.rb' - - 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb' - - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' - - 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb' - - 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/api/v1/admin/reports_controller_spec.rb' - - 'spec/controllers/api/v1/announcements/reactions_controller_spec.rb' - - 'spec/controllers/api/v1/announcements_controller_spec.rb' - - 'spec/controllers/api/v1/apps_controller_spec.rb' - - 'spec/controllers/api/v1/blocks_controller_spec.rb' - - 'spec/controllers/api/v1/bookmarks_controller_spec.rb' - - 'spec/controllers/api/v1/conversations_controller_spec.rb' - - 'spec/controllers/api/v1/custom_emojis_controller_spec.rb' - - 'spec/controllers/api/v1/domain_blocks_controller_spec.rb' - - 'spec/controllers/api/v1/emails/confirmations_controller_spec.rb' - - 'spec/controllers/api/v1/endorsements_controller_spec.rb' - - 'spec/controllers/api/v1/favourites_controller_spec.rb' - - 'spec/controllers/api/v1/filters_controller_spec.rb' - - 'spec/controllers/api/v1/follow_requests_controller_spec.rb' - - 'spec/controllers/api/v1/followed_tags_controller_spec.rb' - - 'spec/controllers/api/v1/instances/activity_controller_spec.rb' - - 'spec/controllers/api/v1/instances/peers_controller_spec.rb' - - 'spec/controllers/api/v1/instances_controller_spec.rb' - - 'spec/controllers/api/v1/lists_controller_spec.rb' - - 'spec/controllers/api/v1/markers_controller_spec.rb' - - 'spec/controllers/api/v1/media_controller_spec.rb' - - 'spec/controllers/api/v1/mutes_controller_spec.rb' - - 'spec/controllers/api/v1/notifications_controller_spec.rb' - - 'spec/controllers/api/v1/polls/votes_controller_spec.rb' - - 'spec/controllers/api/v1/polls_controller_spec.rb' - - 'spec/controllers/api/v1/reports_controller_spec.rb' - - 'spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb' - - 'spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb' - - 'spec/controllers/api/v1/statuses_controller_spec.rb' - - 'spec/controllers/api/v1/suggestions_controller_spec.rb' - - 'spec/controllers/api/v1/tags_controller_spec.rb' - - 'spec/controllers/api/v1/trends/tags_controller_spec.rb' - - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' - - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb' - - 'spec/controllers/api/v2/filters/statuses_controller_spec.rb' - - 'spec/controllers/api/v2/filters_controller_spec.rb' - - 'spec/controllers/api/v2/search_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - - 'spec/controllers/auth/challenges_controller_spec.rb' - - 'spec/controllers/auth/confirmations_controller_spec.rb' - - 'spec/controllers/auth/passwords_controller_spec.rb' - - 'spec/controllers/auth/registrations_controller_spec.rb' - - 'spec/controllers/auth/sessions_controller_spec.rb' - - 'spec/controllers/concerns/account_controller_concern_spec.rb' - - 'spec/controllers/concerns/cache_concern_spec.rb' - - 'spec/controllers/concerns/challengable_concern_spec.rb' - - 'spec/controllers/concerns/export_controller_concern_spec.rb' - - 'spec/controllers/concerns/localized_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - - 'spec/controllers/concerns/user_tracking_concern_spec.rb' - - 'spec/controllers/disputes/appeals_controller_spec.rb' - - 'spec/controllers/disputes/strikes_controller_spec.rb' - - 'spec/controllers/home_controller_spec.rb' - - 'spec/controllers/instance_actors_controller_spec.rb' - - 'spec/controllers/intents_controller_spec.rb' - - 'spec/controllers/oauth/authorizations_controller_spec.rb' - - 'spec/controllers/oauth/tokens_controller_spec.rb' - - 'spec/controllers/settings/imports_controller_spec.rb' - - 'spec/controllers/settings/profiles_controller_spec.rb' - - 'spec/controllers/statuses_cleanup_controller_spec.rb' - - 'spec/controllers/tags_controller_spec.rb' - - 'spec/controllers/well_known/host_meta_controller_spec.rb' - - 'spec/controllers/well_known/nodeinfo_controller_spec.rb' - - 'spec/controllers/well_known/webfinger_controller_spec.rb' - - 'spec/helpers/accounts_helper_spec.rb' - - 'spec/helpers/admin/account_moderation_notes_helper_spec.rb' - - 'spec/helpers/admin/action_logs_helper_spec.rb' - - 'spec/helpers/flashes_helper_spec.rb' - - 'spec/helpers/formatting_helper_spec.rb' - - 'spec/helpers/home_helper_spec.rb' - - 'spec/helpers/routing_helper_spec.rb' - - 'spec/mailers/admin_mailer_spec.rb' - - 'spec/mailers/notification_mailer_spec.rb' - - 'spec/mailers/user_mailer_spec.rb' - - 'spec/models/account/field_spec.rb' - - 'spec/models/account_alias_spec.rb' - - 'spec/models/account_conversation_spec.rb' - - 'spec/models/account_deletion_request_spec.rb' - - 'spec/models/account_domain_block_spec.rb' - - 'spec/models/account_migration_spec.rb' - - 'spec/models/account_moderation_note_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/account_statuses_cleanup_policy_spec.rb' - - 'spec/models/admin/account_action_spec.rb' - - 'spec/models/admin/action_log_spec.rb' - - 'spec/models/announcement_mute_spec.rb' - - 'spec/models/announcement_reaction_spec.rb' - - 'spec/models/announcement_spec.rb' - - 'spec/models/backup_spec.rb' - - 'spec/models/block_spec.rb' - - 'spec/models/canonical_email_block_spec.rb' - - 'spec/models/conversation_mute_spec.rb' - - 'spec/models/conversation_spec.rb' - - 'spec/models/custom_emoji_spec.rb' - - 'spec/models/custom_filter_keyword_spec.rb' - - 'spec/models/custom_filter_spec.rb' - - 'spec/models/device_spec.rb' - - 'spec/models/domain_block_spec.rb' - - 'spec/models/email_domain_block_spec.rb' - - 'spec/models/encrypted_message_spec.rb' - - 'spec/models/favourite_spec.rb' - - 'spec/models/featured_tag_spec.rb' - - 'spec/models/follow_recommendation_suppression_spec.rb' - - 'spec/models/follow_request_spec.rb' - - 'spec/models/follow_spec.rb' - - 'spec/models/home_feed_spec.rb' - - 'spec/models/identity_spec.rb' - - 'spec/models/import_spec.rb' - - 'spec/models/invite_spec.rb' - - 'spec/models/list_account_spec.rb' - - 'spec/models/list_spec.rb' - - 'spec/models/login_activity_spec.rb' - - 'spec/models/media_attachment_spec.rb' - - 'spec/models/mention_spec.rb' - - 'spec/models/mute_spec.rb' - - 'spec/models/notification_spec.rb' - - 'spec/models/poll_vote_spec.rb' - - 'spec/models/preview_card_spec.rb' - - 'spec/models/preview_card_trend_spec.rb' - - 'spec/models/public_feed_spec.rb' - - 'spec/models/relay_spec.rb' - - 'spec/models/scheduled_status_spec.rb' - - 'spec/models/session_activation_spec.rb' - - 'spec/models/setting_spec.rb' - - 'spec/models/site_upload_spec.rb' - - 'spec/models/status_pin_spec.rb' - - 'spec/models/status_spec.rb' - - 'spec/models/status_stat_spec.rb' - - 'spec/models/status_trend_spec.rb' - - 'spec/models/system_key_spec.rb' - - 'spec/models/tag_follow_spec.rb' - - 'spec/models/unavailable_domain_spec.rb' - - 'spec/models/user_invite_request_spec.rb' - - 'spec/models/user_role_spec.rb' - - 'spec/models/user_spec.rb' - - 'spec/models/web/push_subscription_spec.rb' - - 'spec/models/web/setting_spec.rb' - - 'spec/models/webauthn_credentials_spec.rb' - - 'spec/models/webhook_spec.rb' - RSpec/RepeatedExample: Exclude: - 'spec/policies/status_policy_spec.rb' @@ -1120,7 +716,6 @@ RSpec/VerifiedDoubles: - 'spec/controllers/api/web/embeds_controller_spec.rb' - 'spec/controllers/auth/sessions_controller_spec.rb' - 'spec/controllers/disputes/appeals_controller_spec.rb' - - 'spec/controllers/settings/imports_controller_spec.rb' - 'spec/helpers/statuses_helper_spec.rb' - 'spec/lib/suspicious_sign_in_detector_spec.rb' - 'spec/models/account/field_spec.rb' @@ -1148,19 +743,6 @@ RSpec/VerifiedDoubles: - 'spec/workers/feed_insert_worker_spec.rb' - 'spec/workers/regeneration_worker_spec.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/ActiveRecordCallbacksOrder: - Exclude: - - 'app/models/account.rb' - - 'app/models/account_conversation.rb' - - 'app/models/announcement_reaction.rb' - - 'app/models/block.rb' - - 'app/models/media_attachment.rb' - - 'app/models/session_activation.rb' - - 'app/models/status.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Rails/ApplicationController: Exclude: @@ -1216,12 +798,6 @@ Rails/CreateTableWithTimestamps: - 'db/migrate/20220824233535_create_status_trends.rb' - 'db/migrate/20221006061337_create_preview_card_trends.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Severity. -Rails/DeprecatedActiveModelErrorsMethods: - Exclude: - - 'lib/mastodon/accounts_cli.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Severity. Rails/DuplicateAssociation: @@ -1235,74 +811,6 @@ Rails/Exit: Exclude: - 'config/boot.rb' -# Configuration parameters: EnforcedStyle. -# SupportedStyles: slashes, arguments -Rails/FilePath: - Exclude: - - 'app/lib/themes.rb' - - 'app/models/setting.rb' - - 'app/validators/reaction_validator.rb' - - 'config/environments/test.rb' - - 'config/initializers/locale.rb' - - 'db/migrate/20170716191202_add_hide_notifications_to_mute.rb' - - 'db/migrate/20171005171936_add_disabled_to_custom_emojis.rb' - - 'db/migrate/20171028221157_add_reblogs_to_follows.rb' - - 'db/migrate/20171107143332_add_memorial_to_accounts.rb' - - 'db/migrate/20171107143624_add_disabled_to_users.rb' - - 'db/migrate/20171109012327_add_moderator_to_accounts.rb' - - 'db/migrate/20171130000000_add_embed_url_to_preview_cards.rb' - - 'db/migrate/20180615122121_add_autofollow_to_invites.rb' - - 'db/migrate/20180707154237_add_whole_word_to_custom_filter.rb' - - 'db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb' - - 'db/migrate/20181010141500_add_silent_to_mentions.rb' - - 'db/migrate/20181017170937_add_reject_reports_to_domain_blocks.rb' - - 'db/migrate/20181018205649_add_unread_to_account_conversations.rb' - - 'db/migrate/20181127130500_identity_id_to_bigint.rb' - - 'db/migrate/20181127165847_add_show_replies_to_lists.rb' - - 'db/migrate/20190201012802_add_overwrite_to_imports.rb' - - 'db/migrate/20190306145741_add_lock_version_to_polls.rb' - - 'db/migrate/20190307234537_add_approved_to_users.rb' - - 'db/migrate/20191001213028_add_lock_version_to_account_stats.rb' - - 'db/migrate/20191212003415_increase_backup_size.rb' - - 'db/migrate/20200312144258_add_title_to_account_warning_presets.rb' - - 'db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb' - - 'db/migrate/20200917192924_add_notify_to_follows.rb' - - 'db/migrate/20201218054746_add_obfuscate_to_domain_blocks.rb' - - 'db/migrate/20210421121431_add_case_insensitive_btree_index_to_tags.rb' - - 'db/migrate/20211231080958_add_category_to_reports.rb' - - 'db/migrate/20220613110834_add_action_to_custom_filters.rb' - - 'db/post_migrate/20220307083603_optimize_null_index_conversations_uri.rb' - - 'db/post_migrate/20220310060545_optimize_null_index_statuses_in_reply_to_account_id.rb' - - 'db/post_migrate/20220310060556_optimize_null_index_statuses_in_reply_to_id.rb' - - 'db/post_migrate/20220310060614_optimize_null_index_media_attachments_scheduled_status_id.rb' - - 'db/post_migrate/20220310060626_optimize_null_index_media_attachments_shortcode.rb' - - 'db/post_migrate/20220310060641_optimize_null_index_users_reset_password_token.rb' - - 'db/post_migrate/20220310060653_optimize_null_index_users_created_by_application_id.rb' - - 'db/post_migrate/20220310060706_optimize_null_index_statuses_uri.rb' - - 'db/post_migrate/20220310060722_optimize_null_index_accounts_moved_to_account_id.rb' - - 'db/post_migrate/20220310060740_optimize_null_index_oauth_access_tokens_refresh_token.rb' - - 'db/post_migrate/20220310060750_optimize_null_index_accounts_url.rb' - - 'db/post_migrate/20220310060809_optimize_null_index_oauth_access_tokens_resource_owner_id.rb' - - 'db/post_migrate/20220310060833_optimize_null_index_announcement_reactions_custom_emoji_id.rb' - - 'db/post_migrate/20220310060854_optimize_null_index_appeals_approved_by_account_id.rb' - - 'db/post_migrate/20220310060913_optimize_null_index_account_migrations_target_account_id.rb' - - 'db/post_migrate/20220310060926_optimize_null_index_appeals_rejected_by_account_id.rb' - - 'db/post_migrate/20220310060939_optimize_null_index_list_accounts_follow_id.rb' - - 'db/post_migrate/20220310060959_optimize_null_index_web_push_subscriptions_access_token_id.rb' - - 'db/post_migrate/20220613110802_remove_whole_word_from_custom_filters.rb' - - 'db/post_migrate/20220613110903_remove_irreversible_from_custom_filters.rb' - - 'db/post_migrate/20220617202502_migrate_roles.rb' - - 'db/seeds.rb' - - 'db/seeds/03_roles.rb' - - 'lib/tasks/branding.rake' - - 'lib/tasks/emojis.rake' - - 'lib/tasks/repo.rake' - - 'spec/controllers/admin/custom_emojis_controller_spec.rb' - - 'spec/fabricators/custom_emoji_fabricator.rb' - - 'spec/fabricators/site_upload_fabricator.rb' - - 'spec/rails_helper.rb' - - 'spec/spec_helper.rb' - # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasAndBelongsToMany: @@ -1445,12 +953,30 @@ Rails/SkipsModelValidations: - 'spec/services/follow_service_spec.rb' - 'spec/services/update_account_service_spec.rb' -Rails/TransactionExitStatement: +# Configuration parameters: Include. +# Include: db/**/*.rb +Rails/ThreeStateBooleanColumn: Exclude: - - 'app/lib/activitypub/activity/announce.rb' - - 'app/lib/activitypub/activity/create.rb' - - 'app/lib/activitypub/activity/delete.rb' - - 'app/services/activitypub/process_account_service.rb' + - 'db/migrate/20160325130944_add_admin_to_users.rb' + - 'db/migrate/20161123093447_add_sensitive_to_statuses.rb' + - 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb' + - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb' + - 'db/migrate/20170209184350_add_reply_to_statuses.rb' + - 'db/migrate/20170330163835_create_imports.rb' + - 'db/migrate/20170905165803_add_local_to_statuses.rb' + - 'db/migrate/20171210213213_add_local_only_flag_to_statuses.rb' + - 'db/migrate/20181203021853_add_discoverable_to_accounts.rb' + - 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb' + - 'db/migrate/20190805123746_add_capabilities_to_tags.rb' + - 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb' + - 'db/migrate/20200309150742_add_forwarded_to_reports.rb' + - 'db/migrate/20210609202149_create_login_activities.rb' + - 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb' + - 'db/migrate/20211031031021_create_preview_card_providers.rb' + - 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb' + - 'db/migrate/20220202200743_add_trendable_to_accounts.rb' + - 'db/migrate/20220202200926_add_trendable_to_statuses.rb' + - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb' # Configuration parameters: Include. # Include: app/models/**/*.rb @@ -1519,12 +1045,6 @@ Style/CaseEquality: Exclude: - 'config/initializers/trusted_proxies.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: MinBranchesCount. -Style/CaseLikeIf: - Exclude: - - 'app/controllers/concerns/signature_verification.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedMethods, AllowedPatterns. # AllowedMethods: ==, equal?, eql? @@ -1542,16 +1062,10 @@ Style/CombinableLoops: - 'app/models/form/custom_emoji_batch.rb' - 'app/models/form/ip_block_batch.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/ConcatArrayLiterals: - Exclude: - - 'app/lib/feed_manager.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedVars. Style/FetchEnvVar: Exclude: - - 'app/helpers/application_helper.rb' - 'app/lib/redis_configuration.rb' - 'app/lib/translation_service.rb' - 'config/environments/development.rb' @@ -2001,7 +1515,6 @@ Style/GuardClause: - 'app/controllers/auth/passwords_controller.rb' - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb' - 'app/lib/activitypub/activity/block.rb' - - 'app/lib/connection_pool/shared_connection_pool.rb' - 'app/lib/request.rb' - 'app/lib/request_pool.rb' - 'app/lib/webfinger.rb' @@ -2036,7 +1549,6 @@ Style/HashAsLastArrayItem: Exclude: - 'app/controllers/admin/statuses_controller.rb' - 'app/controllers/api/v1/statuses_controller.rb' - - 'app/models/account.rb' - 'app/models/concerns/account_counters.rb' - 'app/models/concerns/status_threading_concern.rb' - 'app/models/status.rb' @@ -2044,19 +1556,6 @@ Style/HashAsLastArrayItem: - 'app/services/notify_service.rb' - 'db/migrate/20181024224956_migrate_account_conversations.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. -# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys -# SupportedShorthandSyntax: always, never, either, consistent -Style/HashSyntax: - Exclude: - - 'app/helpers/application_helper.rb' - - 'app/models/media_attachment.rb' - - 'lib/terrapin/multi_pipe_extensions.rb' - - 'spec/controllers/admin/reports/actions_controller_spec.rb' - - 'spec/controllers/admin/statuses_controller_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Style/HashTransformValues: Exclude: @@ -2074,22 +1573,8 @@ Style/IfUnlessModifier: # Configuration parameters: InverseMethods, InverseBlocks. Style/InverseMethods: Exclude: - - 'app/controllers/concerns/signature_verification.rb' - - 'app/helpers/jsonld_helper.rb' - - 'app/lib/activitypub/activity/create.rb' - - 'app/lib/activitypub/activity/move.rb' - - 'app/lib/feed_manager.rb' - - 'app/lib/link_details_extractor.rb' - - 'app/models/concerns/attachmentable.rb' - - 'app/models/concerns/remotable.rb' - 'app/models/custom_filter.rb' - - 'app/models/webhook.rb' - - 'app/services/activitypub/process_status_update_service.rb' - - 'app/services/fetch_link_card_service.rb' - - 'app/services/search_service.rb' - 'app/services/update_account_service.rb' - - 'app/workers/web/push_notification_worker.rb' - - 'lib/paperclip/color_extractor.rb' - 'spec/controllers/activitypub/replies_controller_spec.rb' # This cop supports safe autocorrection (--autocorrect). @@ -2110,12 +1595,10 @@ Style/MapToHash: # SupportedStyles: literals, strict Style/MutableConstant: Exclude: - - 'app/models/account.rb' - 'app/models/tag.rb' - 'app/services/delete_account_service.rb' - 'config/initializers/twitter_regex.rb' - 'lib/mastodon/migration_warning.rb' - - 'spec/controllers/api/base_controller_spec.rb' # This cop supports safe autocorrection (--autocorrect). Style/NilLambda: @@ -2199,7 +1682,6 @@ Style/RedundantRegexpEscape: Style/RegexpLiteral: Exclude: - 'app/lib/link_details_extractor.rb' - - 'app/lib/permalink_redirector.rb' - 'app/lib/plain_text_formatter.rb' - 'app/lib/tag_manager.rb' - 'app/lib/text_formatter.rb' @@ -2321,11 +1803,14 @@ Style/TrailingCommaInHashLiteral: - 'config/environments/test.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: WordRegex. +# Configuration parameters: EnforcedStyle, MinSize, WordRegex. # SupportedStyles: percent, brackets Style/WordArray: - EnforcedStyle: percent - MinSize: 6 + Exclude: + - 'app/helpers/languages_helper.rb' + - 'config/initializers/cors.rb' + - 'spec/controllers/settings/imports_controller_spec.rb' + - 'spec/models/form/import_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. diff --git a/Aptfile b/Aptfile index 8f5bb72a25..5e033f1365 100644 --- a/Aptfile +++ b/Aptfile @@ -1,4 +1,5 @@ ffmpeg +libopenblas0-pthread libpq-dev libxdamage1 libxfixes3 diff --git a/Dockerfile b/Dockerfile index 9789334211..91c26d2ac0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,6 +41,10 @@ RUN apt-get update && \ FROM node:${NODE_VERSION} +# Use those args to specify your own version flags & suffixes +ARG MASTODON_VERSION_FLAGS="" +ARG MASTODON_VERSION_SUFFIX="" + ARG UID="991" ARG GID="991" @@ -84,7 +88,9 @@ COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon ENV RAILS_ENV="production" \ NODE_ENV="production" \ RAILS_SERVE_STATIC_FILES="true" \ - BIND="0.0.0.0" + BIND="0.0.0.0" \ + MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \ + MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}" # Set the run user USER mastodon diff --git a/Gemfile b/Gemfile index 3301b83cc0..e55b21c9e0 100644 --- a/Gemfile +++ b/Gemfile @@ -30,10 +30,7 @@ gem 'browser' gem 'charlock_holmes', '~> 0.7.7' gem 'chewy', '~> 7.3' gem 'devise', '~> 4.9' -# The below `v4.x` branch allows attr_encrypted 4.x, which is required for Rails 7. -# Once a new gem version is pushed, we can go back to released gem and off of github branch. -gem 'devise-two-factor', github: 'tinfoil/devise-two-factor', branch: 'v4.x' -gem 'attr_encrypted', '~> 4.0' +gem 'devise-two-factor', '~> 4.1' group :pam_authentication, optional: true do gem 'devise_pam_authenticatable2', '~> 9.2' @@ -164,3 +161,4 @@ gem 'hcaptcha', '~> 7.1' gem 'cocoon', '~> 1.2' gem 'net-http', '~> 0.3.2' +gem 'rubyzip', '~> 2.3' diff --git a/Gemfile.lock b/Gemfile.lock index 7cf23180e0..f22d5b3721 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -27,18 +27,6 @@ GIT rails-settings-cached (0.6.6) rails (>= 4.2.0) -GIT - remote: https://github.com/tinfoil/devise-two-factor.git - revision: e685f91ce62d036259885fbe31fcb4fa930bcfcb - branch: v4.x - specs: - devise-two-factor (4.0.2) - activesupport (< 7.1) - attr_encrypted (>= 1.3, < 5, != 2) - devise (~> 4.0) - railties (< 7.1) - rotp (~> 6.0) - GEM remote: https://rubygems.org/ specs: @@ -218,6 +206,12 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) + devise-two-factor (4.1.0) + activesupport (< 7.1) + attr_encrypted (>= 1.3, < 5, != 2) + devise (~> 4.0) + railties (< 7.1) + rotp (~> 6.0) devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) rpam2 (~> 4.0) @@ -354,15 +348,15 @@ GEM ipaddress (0.8.3) jmespath (1.6.2) json (2.6.3) - json-canonicalization (0.3.1) + json-canonicalization (0.3.2) json-jwt (1.15.3) activesupport (>= 4.2) aes_key_wrap bindata httpclient - json-ld (3.2.4) + json-ld (3.2.5) htmlentities (~> 4.3) - json-canonicalization (~> 0.3) + json-canonicalization (~> 0.3, >= 0.3.2) link_header (~> 0.0, >= 0.0.8) multi_json (~> 1.15) rack (>= 2.2, < 4) @@ -492,7 +486,7 @@ GEM parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.5.2) + pg (1.5.3) pghero (3.3.3) activerecord (>= 6) pkg-config (1.5.1) @@ -626,7 +620,7 @@ GEM rubocop-performance (1.17.1) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.18.0) + rubocop-rails (2.19.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) @@ -638,6 +632,7 @@ GEM nokogiri (>= 1.10.5) rexml ruby2_keywords (0.0.5) + rubyzip (2.3.2) rufus-scheduler (3.8.2) fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) @@ -777,7 +772,6 @@ DEPENDENCIES active_model_serializers (~> 0.10) addressable (~> 2.8) annotate (~> 3.2) - attr_encrypted (~> 4.0) aws-sdk-s3 (~> 1.120) better_errors (~> 2.9) binding_of_caller (~> 1.0) @@ -799,7 +793,7 @@ DEPENDENCIES concurrent-ruby connection_pool devise (~> 4.9) - devise-two-factor! + devise-two-factor (~> 4.1) devise_pam_authenticatable2 (~> 9.2) discard (~> 1.2) doorkeeper (~> 5.6) @@ -879,6 +873,7 @@ DEPENDENCIES rubocop-rails rubocop-rspec ruby-progressbar (~> 1.13) + rubyzip (~> 2.3) sanitize (~> 6.0) scenic (~> 1.7) sidekiq (~> 6.5) diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 750f5c995c..081550b762 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -33,7 +33,7 @@ module Admin if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save - flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety + flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe @domain_block.errors.delete(:domain) render :new else diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index 7485438dbf..5ea26d55bd 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -15,7 +15,8 @@ class Api::V1::MediaController < Api::BaseController render json: @media_attachment, serializer: REST::MediaAttachmentSerializer rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 - rescue Paperclip::Error + rescue Paperclip::Error => e + Rails.logger.error "#{e.class}: #{e.message}" render json: processing_error, status: 500 end diff --git a/app/controllers/api/v2/media_controller.rb b/app/controllers/api/v2/media_controller.rb index 288f847f17..72bc694421 100644 --- a/app/controllers/api/v2/media_controller.rb +++ b/app/controllers/api/v2/media_controller.rb @@ -6,7 +6,8 @@ class Api::V2::MediaController < Api::V1::MediaController render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: @media_attachment.not_processed? ? 202 : 200 rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 - rescue Paperclip::Error + rescue Paperclip::Error => e + Rails.logger.error "#{e.class}: #{e.message}" render json: processing_error, status: 500 end end diff --git a/app/controllers/authorize_interactions_controller.rb b/app/controllers/authorize_interactions_controller.rb index 97fe4a9abd..73f0f2b88d 100644 --- a/app/controllers/authorize_interactions_controller.rb +++ b/app/controllers/authorize_interactions_controller.rb @@ -60,7 +60,7 @@ class AuthorizeInteractionsController < ApplicationController end def uri_param - params[:uri] || params.fetch(:acct, '').gsub(/\Aacct:/, '') + params[:uri] || params.fetch(:acct, '').delete_prefix('acct:') end def set_body_classes diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 9317259433..1d27c92c8c 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -180,14 +180,15 @@ module SignatureVerification def build_signed_string signed_headers.map do |signed_header| - if signed_header == Request::REQUEST_TARGET + case signed_header + when Request::REQUEST_TARGET "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" - elsif signed_header == '(created)' + when '(created)' raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019' raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank? "(created): #{signature_params['created']}" - elsif signed_header == '(expires)' + when '(expires)' raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019' raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank? @@ -244,7 +245,7 @@ module SignatureVerification end if key_id.start_with?('acct:') - stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) } + stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) account = ActivityPub::TagManager.instance.uri_to_actor(key_id) account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) } diff --git a/app/controllers/intents_controller.rb b/app/controllers/intents_controller.rb index ca89fc7fe6..ea024e30e6 100644 --- a/app/controllers/intents_controller.rb +++ b/app/controllers/intents_controller.rb @@ -9,7 +9,7 @@ class IntentsController < ApplicationController if uri.scheme == 'web+mastodon' case uri.host when 'follow' - return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].gsub(/\Aacct:/, '')) + return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].delete_prefix('acct:')) when 'share' return redirect_to share_path(text: uri.query_values['text']) end diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 1b5486c122..8d480d704e 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -16,7 +16,7 @@ class MediaProxyController < ApplicationController rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error def show - with_lock("media_download:#{params[:id]}") do + with_redis_lock("media_download:#{params[:id]}") do @media_attachment = MediaAttachment.remote.attached.find(params[:id]) authorize @media_attachment.status, :show? redownload! if @media_attachment.needs_redownload? && !reject_media? diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index deaa7940eb..46a340aeb3 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -15,7 +15,7 @@ class Settings::ExportsController < Settings::BaseController def create backup = nil - with_lock("backup:#{current_user.id}") do + with_redis_lock("backup:#{current_user.id}") do authorize :backup, :create? backup = current_user.backups.create! end diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index d4516526ee..bdbf8796fe 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -1,31 +1,97 @@ # frozen_string_literal: true -class Settings::ImportsController < Settings::BaseController - before_action :set_account +require 'csv' - def show - @import = Import.new +class Settings::ImportsController < Settings::BaseController + before_action :set_bulk_import, only: [:show, :confirm, :destroy] + before_action :set_recent_imports, only: [:index] + + TYPE_TO_FILENAME_MAP = { + following: 'following_accounts_failures.csv', + blocking: 'blocked_accounts_failures.csv', + muting: 'muted_accounts_failures.csv', + domain_blocking: 'blocked_domains_failures.csv', + bookmarks: 'bookmarks_failures.csv', + }.freeze + + TYPE_TO_HEADERS_MAP = { + following: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'], + blocking: false, + muting: ['Account address', 'Hide notifications'], + domain_blocking: false, + bookmarks: false, + }.freeze + + def index + @import = Form::Import.new(current_account: current_account) + end + + def show; end + + def failures + @bulk_import = current_account.bulk_imports.where(state: :finished).find(params[:id]) + + respond_to do |format| + format.csv do + filename = TYPE_TO_FILENAME_MAP[@bulk_import.type.to_sym] + headers = TYPE_TO_HEADERS_MAP[@bulk_import.type.to_sym] + + export_data = CSV.generate(headers: headers, write_headers: true) do |csv| + @bulk_import.rows.find_each do |row| + case @bulk_import.type.to_sym + when :following + csv << [row.data['acct'], row.data.fetch('show_reblogs', true), row.data.fetch('notify', false), row.data['languages']&.join(', ')] + when :blocking + csv << [row.data['acct']] + when :muting + csv << [row.data['acct'], row.data.fetch('hide_notifications', true)] + when :domain_blocking + csv << [row.data['domain']] + when :bookmarks + csv << [row.data['uri']] + end + end + end + + send_data export_data, filename: filename + end + end + end + + def confirm + @bulk_import.update!(state: :scheduled) + BulkImportWorker.perform_async(@bulk_import.id) + redirect_to settings_imports_path, notice: I18n.t('imports.success') end def create - @import = Import.new(import_params) - @import.account = @account + @import = Form::Import.new(import_params.merge(current_account: current_account)) if @import.save - ImportWorker.perform_async(@import.id) - redirect_to settings_import_path, notice: I18n.t('imports.success') + redirect_to settings_import_path(@import.bulk_import.id) else - render :show + # We need to set recent imports as we are displaying the index again + set_recent_imports + render :index end end + def destroy + @bulk_import.destroy! + redirect_to settings_imports_path + end + private - def set_account - @account = current_user.account + def import_params + params.require(:form_import).permit(:data, :type, :mode) end - def import_params - params.require(:import).permit(:data, :type, :mode) + def set_bulk_import + @bulk_import = current_account.bulk_imports.where(state: :unconfirmed).find(params[:id]) + end + + def set_recent_imports + @recent_imports = current_account.bulk_imports.reorder(id: :desc).limit(10) end end diff --git a/app/controllers/settings/preferences/appearance_controller.rb b/app/controllers/settings/preferences/appearance_controller.rb index 80ea57bd2d..4d7d12bb7f 100644 --- a/app/controllers/settings/preferences/appearance_controller.rb +++ b/app/controllers/settings/preferences/appearance_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::Preferences::AppearanceController < Settings::PreferencesController +class Settings::Preferences::AppearanceController < Settings::Preferences::BaseController private def after_update_redirect_path diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences/base_controller.rb similarity index 81% rename from app/controllers/settings/preferences_controller.rb rename to app/controllers/settings/preferences/base_controller.rb index 281deb64d1..faf778a7e5 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences/base_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::PreferencesController < Settings::BaseController +class Settings::Preferences::BaseController < Settings::BaseController def show; end def update @@ -15,7 +15,7 @@ class Settings::PreferencesController < Settings::BaseController private def after_update_redirect_path - settings_preferences_path + raise 'Override in controller' end def user_params diff --git a/app/controllers/settings/preferences/notifications_controller.rb b/app/controllers/settings/preferences/notifications_controller.rb index a16ae6a672..66d6c9a2f7 100644 --- a/app/controllers/settings/preferences/notifications_controller.rb +++ b/app/controllers/settings/preferences/notifications_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::Preferences::NotificationsController < Settings::PreferencesController +class Settings::Preferences::NotificationsController < Settings::Preferences::BaseController private def after_update_redirect_path diff --git a/app/controllers/settings/preferences/other_controller.rb b/app/controllers/settings/preferences/other_controller.rb index 07eb89a762..a19fbf5c48 100644 --- a/app/controllers/settings/preferences/other_controller.rb +++ b/app/controllers/settings/preferences/other_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::Preferences::OtherController < Settings::PreferencesController +class Settings::Preferences::OtherController < Settings::Preferences::BaseController private def after_update_redirect_path diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index a06253f456..0d897e8e24 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -18,7 +18,14 @@ module WellKnown private def set_account - @account = Account.find_local!(username_from_resource) + username = username_from_resource + @account = begin + if username == Rails.configuration.x.local_domain + Account.representative + else + Account.find_local!(username) + end + end end def username_from_resource diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 879752cf76..3192c7ab56 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -32,10 +32,6 @@ module ApplicationHelper paths.any? { |path| current_page?(path) } ? 'active' : '' end - def active_link_to(label, path, **options) - link_to label, path, options.merge(class: active_nav_class(path)) - end - def show_landing_strip? !user_signed_in? && !single_user_mode? end @@ -147,7 +143,7 @@ module ApplicationHelper if prefers_autoplay? image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:") else - image_tag(custom_emoji.image.url(:static), class: 'emojione custom-emoji', alt: ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static))) + image_tag(custom_emoji.image.url(:static), :class => 'emojione custom-emoji', :alt => ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static))) end end @@ -174,11 +170,11 @@ module ApplicationHelper end def storage_host - "https://#{ENV['S3_ALIAS_HOST'].presence || ENV['S3_CLOUDFRONT_HOST']}" + URI::HTTPS.build(host: storage_host_name).to_s end def storage_host? - ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present? + storage_host_name.present? end def quote_wrap(text, line_width: 80, break_sequence: "\n") @@ -236,4 +232,10 @@ module ApplicationHelper def prerender_custom_emojis(html, custom_emojis, other_options = {}) EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s end + + private + + def storage_host_name + ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil) + end end diff --git a/app/javascript/flavours/glitch/actions/app.js b/app/javascript/flavours/glitch/actions/app.js deleted file mode 100644 index de2d93e292..0000000000 --- a/app/javascript/flavours/glitch/actions/app.js +++ /dev/null @@ -1,6 +0,0 @@ -export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE'; - -export const changeLayout = layout => ({ - type: APP_LAYOUT_CHANGE, - layout, -}); diff --git a/app/javascript/flavours/glitch/actions/app.ts b/app/javascript/flavours/glitch/actions/app.ts new file mode 100644 index 0000000000..1fc4416090 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/app.ts @@ -0,0 +1,7 @@ +import { createAction } from '@reduxjs/toolkit'; + +type ChangeLayoutPayload = { + layout: 'mobile' | 'single-column' | 'multi-column'; +}; +export const changeLayout = + createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE'); diff --git a/app/javascript/flavours/glitch/blurhash.js b/app/javascript/flavours/glitch/blurhash.ts similarity index 87% rename from app/javascript/flavours/glitch/blurhash.js rename to app/javascript/flavours/glitch/blurhash.ts index 5adcc3e770..cb1c3b2c82 100644 --- a/app/javascript/flavours/glitch/blurhash.js +++ b/app/javascript/flavours/glitch/blurhash.ts @@ -84,7 +84,7 @@ const DIGIT_CHARACTERS = [ '~', ]; -export const decode83 = (str) => { +export const decode83 = (str: string) => { let value = 0; let c, digit; @@ -97,13 +97,13 @@ export const decode83 = (str) => { return value; }; -export const intToRGB = int => ({ +export const intToRGB = (int: number) => ({ r: Math.max(0, (int >> 16)), g: Math.max(0, (int >> 8) & 255), b: Math.max(0, (int & 255)), }); -export const getAverageFromBlurhash = blurhash => { +export const getAverageFromBlurhash = (blurhash: string) => { if (!blurhash) { return null; } diff --git a/app/javascript/mastodon/compare_id.js b/app/javascript/flavours/glitch/compare_id.ts similarity index 72% rename from app/javascript/mastodon/compare_id.js rename to app/javascript/flavours/glitch/compare_id.ts index d2bd74f447..ae4ac6f897 100644 --- a/app/javascript/mastodon/compare_id.js +++ b/app/javascript/flavours/glitch/compare_id.ts @@ -1,4 +1,4 @@ -export default function compareId (id1, id2) { +export default function compareId (id1: string, id2: string) { if (id1 === id2) { return 0; } diff --git a/app/javascript/flavours/glitch/components/avatar.tsx b/app/javascript/flavours/glitch/components/avatar.tsx index a21ffc9888..d6a9621462 100644 --- a/app/javascript/flavours/glitch/components/avatar.tsx +++ b/app/javascript/flavours/glitch/components/avatar.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import classNames from 'classnames'; import { autoPlayGif } from 'flavours/glitch/initial_state'; import { useHovering } from 'hooks/useHovering'; -import type { Account } from 'types/resources'; +import type { Account } from 'flavours/glitch/types/resources'; type Props = { account: Account | undefined; diff --git a/app/javascript/flavours/glitch/components/blurhash.jsx b/app/javascript/flavours/glitch/components/blurhash.jsx deleted file mode 100644 index f5c58e04ef..0000000000 --- a/app/javascript/flavours/glitch/components/blurhash.jsx +++ /dev/null @@ -1,65 +0,0 @@ -// @ts-check - -import { decode } from 'blurhash'; -import React, { useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; - -/** - * @typedef BlurhashPropsBase - * @property {string?} hash Hash to render - * @property {number} width - * Width of the blurred region in pixels. Defaults to 32 - * @property {number} [height] - * Height of the blurred region in pixels. Defaults to width - * @property {boolean} [dummy] - * Whether dummy mode is enabled. If enabled, nothing is rendered - * and canvas left untouched - */ - -/** @typedef {JSX.IntrinsicElements['canvas'] & BlurhashPropsBase} BlurhashProps */ - -/** - * Component that is used to render blurred of blurhash string - * @param {BlurhashProps} param1 Props of the component - * @returns {JSX.Element} Canvas which will render blurred region element to embed - */ -function Blurhash({ - hash, - width = 32, - height = width, - dummy = false, - ...canvasProps -}) { - const canvasRef = /** @type {import('react').MutableRefObject<HTMLCanvasElement>} */ (useRef()); - - useEffect(() => { - const { current: canvas } = canvasRef; - canvas.width = canvas.width; // resets canvas - - if (dummy || !hash) return; - - try { - const pixels = decode(hash, width, height); - const ctx = canvas.getContext('2d'); - const imageData = new ImageData(pixels, width, height); - - // @ts-expect-error - ctx.putImageData(imageData, 0, 0); - } catch (err) { - console.error('Blurhash decoding failure', { err, hash }); - } - }, [dummy, hash, width, height]); - - return ( - <canvas {...canvasProps} ref={canvasRef} width={width} height={height} /> - ); -} - -Blurhash.propTypes = { - hash: PropTypes.string.isRequired, - width: PropTypes.number, - height: PropTypes.number, - dummy: PropTypes.bool, -}; - -export default React.memo(Blurhash); diff --git a/app/javascript/flavours/glitch/components/blurhash.tsx b/app/javascript/flavours/glitch/components/blurhash.tsx new file mode 100644 index 0000000000..6fec6e1ef7 --- /dev/null +++ b/app/javascript/flavours/glitch/components/blurhash.tsx @@ -0,0 +1,45 @@ +import { decode } from 'blurhash'; +import React, { useRef, useEffect } from 'react'; + +type Props = { + hash: string; + width?: number; + height?: number; + dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched + children?: never; + [key: string]: any; +} +function Blurhash({ + hash, + width = 32, + height = width, + dummy = false, + ...canvasProps +}: Props) { + const canvasRef = useRef<HTMLCanvasElement>(null); + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const canvas = canvasRef.current!; + // eslint-disable-next-line no-self-assign + canvas.width = canvas.width; // resets canvas + + if (dummy || !hash) return; + + try { + const pixels = decode(hash, width, height); + const ctx = canvas.getContext('2d'); + const imageData = new ImageData(pixels, width, height); + + ctx?.putImageData(imageData, 0, 0); + } catch (err) { + console.error('Blurhash decoding failure', { err, hash }); + } + }, [dummy, hash, width, height]); + + return ( + <canvas {...canvasProps} ref={canvasRef} width={width} height={height} /> + ); +} + +export default React.memo(Blurhash); diff --git a/app/javascript/flavours/glitch/components/icon_button.jsx b/app/javascript/flavours/glitch/components/icon_button.tsx similarity index 69% rename from app/javascript/flavours/glitch/components/icon_button.jsx rename to app/javascript/flavours/glitch/components/icon_button.tsx index 93640dd0f2..2bda4ddf34 100644 --- a/app/javascript/flavours/glitch/components/icon_button.jsx +++ b/app/javascript/flavours/glitch/components/icon_button.tsx @@ -1,35 +1,37 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; -import Icon from 'flavours/glitch/components/icon'; -import AnimatedNumber from 'flavours/glitch/components/animated_number'; +import { Icon } from './icon'; +import { AnimatedNumber } from './animated_number'; -export default class IconButton extends React.PureComponent { - - static propTypes = { - className: PropTypes.string, - title: PropTypes.string.isRequired, - icon: PropTypes.string.isRequired, - onClick: PropTypes.func, - onMouseDown: PropTypes.func, - onKeyDown: PropTypes.func, - onKeyPress: PropTypes.func, - size: PropTypes.number, - active: PropTypes.bool, - expanded: PropTypes.bool, - style: PropTypes.object, - activeStyle: PropTypes.object, - disabled: PropTypes.bool, - inverted: PropTypes.bool, - animate: PropTypes.bool, - overlay: PropTypes.bool, - tabIndex: PropTypes.number, - label: PropTypes.string, - counter: PropTypes.number, - obfuscateCount: PropTypes.bool, - href: PropTypes.string, - ariaHidden: PropTypes.bool, - }; +type Props = { + className?: string; + title: string; + icon: string; + onClick?: React.MouseEventHandler<HTMLButtonElement>; + onMouseDown?: React.MouseEventHandler<HTMLButtonElement>; + onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>; + onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>; + size: number; + active: boolean; + expanded?: boolean; + style?: React.CSSProperties; + activeStyle?: React.CSSProperties; + disabled: boolean; + inverted?: boolean; + animate: boolean; + overlay: boolean; + tabIndex: number; + label: string; + counter?: number; + obfuscateCount?: boolean; + href?: string; + ariaHidden: boolean; +} +type States = { + activate: boolean, + deactivate: boolean, +} +export default class IconButton extends React.PureComponent<Props, States> { static defaultProps = { size: 18, @@ -46,7 +48,7 @@ export default class IconButton extends React.PureComponent { deactivate: false, }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps: Props) { if (!nextProps.animate) return; if (this.props.active && !nextProps.active) { @@ -56,27 +58,27 @@ export default class IconButton extends React.PureComponent { } } - handleClick = (e) => { + handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => { e.preventDefault(); - if (!this.props.disabled) { + if (!this.props.disabled && this.props.onClick != null) { this.props.onClick(e); } }; - handleKeyPress = (e) => { + handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> = (e) => { if (this.props.onKeyPress && !this.props.disabled) { this.props.onKeyPress(e); } }; - handleMouseDown = (e) => { + handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (e) => { if (!this.props.disabled && this.props.onMouseDown) { this.props.onMouseDown(e); } }; - handleKeyDown = (e) => { + handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (e) => { if (!this.props.disabled && this.props.onKeyDown) { this.props.onKeyDown(e); } @@ -89,7 +91,7 @@ export default class IconButton extends React.PureComponent { containerSize = `${this.props.size * 1.28571429}px`; } - let style = { + const style = { fontSize: `${this.props.size}px`, height: containerSize, lineHeight: `${this.props.size}px`, @@ -144,7 +146,7 @@ export default class IconButton extends React.PureComponent { </React.Fragment> ); - if (href && !this.prop) { + if (href != null) { contents = ( <a href={href} target='_blank' rel='noopener noreferrer'> {contents} diff --git a/app/javascript/flavours/glitch/components/media_gallery.jsx b/app/javascript/flavours/glitch/components/media_gallery.jsx index 4c86341c9b..9bbde3b5e9 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.jsx +++ b/app/javascript/flavours/glitch/components/media_gallery.jsx @@ -101,12 +101,10 @@ class Item extends React.PureComponent { render () { const { attachment, lang, index, size, standalone, letterbox, displayWidth, visible } = this.props; + let badges = [], thumbnail; + let width = 50; let height = 100; - let top = 'auto'; - let left = 'auto'; - let bottom = 'auto'; - let right = 'auto'; if (size === 1) { width = 100; @@ -116,45 +114,13 @@ class Item extends React.PureComponent { height = 50; } - if (size === 2) { - if (index === 0) { - right = '2px'; - } else { - left = '2px'; - } - } else if (size === 3) { - if (index === 0) { - right = '2px'; - } else if (index > 0) { - left = '2px'; - } - - if (index === 1) { - bottom = '2px'; - } else if (index > 1) { - top = '2px'; - } - } else if (size === 4) { - if (index === 0 || index === 2) { - right = '2px'; - } - - if (index === 1 || index === 3) { - left = '2px'; - } - - if (index < 2) { - bottom = '2px'; - } else { - top = '2px'; - } + if (attachment.get('description')?.length > 0) { + badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>); } - let thumbnail = ''; - if (attachment.get('type') === 'unknown') { return ( - <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> + <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}> <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'> <Blurhash hash={attachment.get('blurhash')} @@ -205,6 +171,8 @@ class Item extends React.PureComponent { } else if (attachment.get('type') === 'gifv') { const autoPlay = this.getAutoPlay(); + badges.push(<span key='gif' className='media-gallery__gifv__label'>GIF</span>); + thumbnail = ( <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> <video @@ -222,14 +190,12 @@ class Item extends React.PureComponent { loop muted /> - - <span className='media-gallery__gifv__label'>GIF</span> </div> ); } return ( - <div className={classNames('media-gallery__item', { standalone, letterbox })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> + <div className={classNames('media-gallery__item', { standalone, letterbox, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}> <Blurhash hash={attachment.get('blurhash')} dummy={!useBlurhash} @@ -237,7 +203,14 @@ class Item extends React.PureComponent { 'media-gallery__preview--hidden': visible && this.state.loaded, })} /> + {visible && thumbnail} + + {badges && ( + <div className='media-gallery__item__badges'> + {badges} + </div> + )} </div> ); } @@ -358,12 +331,10 @@ class MediaGallery extends React.PureComponent { const computedClass = classNames('media-gallery', { 'full-width': fullwidth }); - if (this.isStandaloneEligible() && width) { - style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); - } else if (width) { - style.height = width / (16/9); + if (this.isStandaloneEligible()) { // TODO: cropImages setting + style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`; } else { - return (<div className={computedClass} ref={this.handleRef} />); + style.aspectRatio = '16 / 9'; } if (this.isStandaloneEligible()) { diff --git a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx index 9d1b7f55a2..7e02556749 100644 --- a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx +++ b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx @@ -3,62 +3,22 @@ import PropTypes from 'prop-types'; import Icon from 'flavours/glitch/components/icon'; import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; import { connect } from 'react-redux'; -import { debounce } from 'lodash'; import { FormattedMessage } from 'react-intl'; class PictureInPicturePlaceholder extends React.PureComponent { static propTypes = { - width: PropTypes.number, dispatch: PropTypes.func.isRequired, }; - state = { - width: this.props.width, - height: this.props.width && (this.props.width / (16/9)), - }; - handleClick = () => { const { dispatch } = this.props; dispatch(removePictureInPicture()); }; - setRef = c => { - this.node = c; - - if (this.node) { - this._setDimensions(); - } - }; - - _setDimensions () { - const width = this.node.offsetWidth; - const height = width / (16/9); - - this.setState({ width, height }); - } - - componentDidMount () { - window.addEventListener('resize', this.handleResize, { passive: true }); - } - - componentWillUnmount () { - window.removeEventListener('resize', this.handleResize); - } - - handleResize = debounce(() => { - if (this.node) { - this._setDimensions(); - } - }, 250, { - trailing: true, - }); - render () { - const { height } = this.state; - return ( - <div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex={0} onClick={this.handleClick}> + <div className='picture-in-picture-placeholder' role='button' tabIndex={0} onClick={this.handleClick}> <Icon id='window-restore' /> <FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' /> </div> diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index a5843a4e54..25eac0d452 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -624,7 +624,7 @@ class Status extends ImmutablePureComponent { attachments = status.get('media_attachments'); if (pictureInPicture.get('inUse')) { - media.push(<PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />); + media.push(<PictureInPicturePlaceholder />); mediaIcons.push('video-camera'); } else if (attachments.size > 0) { if (muted || attachments.some(item => item.get('type') === 'unknown')) { @@ -680,8 +680,6 @@ class Status extends ImmutablePureComponent { fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])} preventPlayback={isCollapsed || !isExpanded} onOpenVideo={this.handleOpenVideo} - width={this.props.cachedMediaWidth} - cacheWidth={this.props.cacheMediaWidth} deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined} visible={this.state.showMedia} onToggleVisibility={this.handleToggleMediaVisibility} @@ -721,8 +719,6 @@ class Status extends ImmutablePureComponent { onOpenMedia={this.handleOpenMedia} card={status.get('card')} compact - cacheWidth={this.props.cacheMediaWidth} - defaultWidth={this.props.cachedMediaWidth} sensitive={status.get('sensitive')} />, ); diff --git a/app/javascript/flavours/glitch/containers/compose_container.jsx b/app/javascript/flavours/glitch/containers/compose_container.jsx index 1e49b89a0b..dada4cfed7 100644 --- a/app/javascript/flavours/glitch/containers/compose_container.jsx +++ b/app/javascript/flavours/glitch/containers/compose_container.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import PropTypes from 'prop-types'; -import configureStore from 'flavours/glitch/store/configureStore'; +import { store } from 'flavours/glitch/store/configureStore'; import { hydrateStore } from 'flavours/glitch/actions/store'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from 'mastodon/locales'; @@ -12,8 +12,6 @@ import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; const { localeData, messages } = getLocale(); addLocaleData(localeData); -const store = configureStore(); - if (initialState) { store.dispatch(hydrateStore(initialState)); } diff --git a/app/javascript/flavours/glitch/containers/mastodon.jsx b/app/javascript/flavours/glitch/containers/mastodon.jsx index dd7623a813..aca7f4dc59 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.jsx +++ b/app/javascript/flavours/glitch/containers/mastodon.jsx @@ -5,7 +5,7 @@ import { IntlProvider, addLocaleData } from 'react-intl'; import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter, Route } from 'react-router-dom'; import { ScrollContext } from 'react-router-scroll-4'; -import configureStore from 'flavours/glitch/store/configureStore'; +import { store } from 'flavours/glitch/store/configureStore'; import UI from 'flavours/glitch/features/ui'; import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; import { hydrateStore } from 'flavours/glitch/actions/store'; @@ -20,7 +20,6 @@ addLocaleData(localeData); const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`; -export const store = configureStore(); const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); diff --git a/app/javascript/flavours/glitch/features/audio/index.jsx b/app/javascript/flavours/glitch/features/audio/index.jsx index f5bffb9738..c3e4ed5d4f 100644 --- a/app/javascript/flavours/glitch/features/audio/index.jsx +++ b/app/javascript/flavours/glitch/features/audio/index.jsx @@ -390,7 +390,7 @@ class Audio extends React.PureComponent { } _getRadius () { - return parseInt(((this.state.height || this.props.height) - (PADDING * this._getScaleCoefficient()) * 2) / 2); + return parseInt((this.state.height || this.props.height) / 2 - PADDING * this._getScaleCoefficient()); } _getScaleCoefficient () { @@ -402,7 +402,7 @@ class Audio extends React.PureComponent { } _getCY() { - return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient())); + return Math.floor((this.state.height || this.props.height) / 2); } _getAccentColor () { @@ -476,7 +476,7 @@ class Audio extends React.PureComponent { } return ( - <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}> + <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}> <Blurhash hash={blurhash} @@ -521,9 +521,16 @@ class Audio extends React.PureComponent { {(revealed || editable) && <img src={this.props.poster} alt='' - width={(this._getRadius() - TICK_SIZE) * 2} - height={(this._getRadius() - TICK_SIZE) * 2} - style={{ position: 'absolute', left: this._getCX(), top: this._getCY(), transform: 'translate(-50%, -50%)', borderRadius: '50%', pointerEvents: 'none' }} + style={{ + position: 'absolute', + left: '50%', + top: '50%', + height: `calc(${(100 - 2 * 100 * PADDING / 982)}% - ${TICK_SIZE * 2}px)`, + aspectRatio: '1', + transform: 'translate(-50%, -50%)', + borderRadius: '50%', + pointerEvents: 'none', + }} />} <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> diff --git a/app/javascript/flavours/glitch/features/status/components/card.jsx b/app/javascript/flavours/glitch/features/status/components/card.jsx index 0d5f781a02..af3fe4bf09 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.jsx +++ b/app/javascript/flavours/glitch/features/status/components/card.jsx @@ -8,7 +8,6 @@ import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; import Icon from 'flavours/glitch/components/icon'; import { useBlurhash } from 'flavours/glitch/initial_state'; import Blurhash from 'flavours/glitch/components/blurhash'; -import { debounce } from 'lodash'; const getHostname = url => { const parser = document.createElement('a'); @@ -45,8 +44,6 @@ export default class Card extends React.PureComponent { card: ImmutablePropTypes.map, onOpenMedia: PropTypes.func.isRequired, compact: PropTypes.bool, - defaultWidth: PropTypes.number, - cacheWidth: PropTypes.func, sensitive: PropTypes.bool, }; @@ -55,7 +52,6 @@ export default class Card extends React.PureComponent { }; state = { - width: this.props.defaultWidth || 280, previewLoaded: false, embedded: false, revealed: !this.props.sensitive, @@ -78,24 +74,6 @@ export default class Card extends React.PureComponent { window.removeEventListener('resize', this.handleResize); } - _setDimensions () { - const width = this.node.offsetWidth; - - if (this.props.cacheWidth) { - this.props.cacheWidth(width); - } - - this.setState({ width }); - } - - handleResize = debounce(() => { - if (this.node) { - this._setDimensions(); - } - }, 250, { - trailing: true, - }); - handlePhotoClick = () => { const { card, onOpenMedia } = this.props; @@ -129,10 +107,6 @@ export default class Card extends React.PureComponent { setRef = c => { this.node = c; - - if (this.node) { - this._setDimensions(); - } }; handleImageLoad = () => { @@ -148,36 +122,31 @@ export default class Card extends React.PureComponent { renderVideo () { const { card } = this.props; const content = { __html: addAutoPlay(card.get('html')) }; - const { width } = this.state; - const ratio = card.get('width') / card.get('height'); - const height = width / ratio; return ( <div ref={this.setRef} className='status-card__image status-card-video' dangerouslySetInnerHTML={content} - style={{ height }} + style={{ aspectRatio: `${card.get('width')} / ${card.get('height')}` }} /> ); } render () { const { card, compact } = this.props; - const { width, embedded, revealed } = this.state; + const { embedded, revealed } = this.state; if (card === null) { return null; } const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); - const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded; + const horizontal = (!compact && card.get('width') > card.get('height')) || card.get('type') !== 'link' || embedded; const interactive = card.get('type') !== 'link'; const className = classnames('status-card', { horizontal, compact, interactive }); const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; const language = card.get('language') || ''; - const ratio = card.get('width') / card.get('height'); - const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); const description = ( <div className='status-card__content' lang={language}> @@ -187,6 +156,14 @@ export default class Card extends React.PureComponent { </div> ); + const thumbnailStyle = { + visibility: revealed? null : 'hidden', + }; + + if (horizontal) { + thumbnailStyle.aspectRatio = (compact && !embedded) ? '16 / 9' : `${card.get('width')} / ${card.get('height')}`; + } + let embed = ''; let canvas = ( <Blurhash @@ -197,7 +174,7 @@ export default class Card extends React.PureComponent { dummy={!useBlurhash} /> ); - let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />; + let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />; let spoilerButton = ( <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'> <span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx index aee6743dcd..cc7937e5e2 100644 --- a/app/javascript/flavours/glitch/features/ui/index.jsx +++ b/app/javascript/flavours/glitch/features/ui/index.jsx @@ -378,7 +378,7 @@ class UI extends React.Component { if (layout !== this.props.layout) { this.handleLayoutChange.cancel(); - this.props.dispatch(changeLayout(layout)); + this.props.dispatch(changeLayout({ layout })); } else { this.handleLayoutChange(); } diff --git a/app/javascript/flavours/glitch/features/video/index.jsx b/app/javascript/flavours/glitch/features/video/index.jsx index 0a9db059eb..78d36edb70 100644 --- a/app/javascript/flavours/glitch/features/video/index.jsx +++ b/app/javascript/flavours/glitch/features/video/index.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { is } from 'immutable'; -import { throttle, debounce } from 'lodash'; +import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; @@ -102,8 +102,6 @@ class Video extends React.PureComponent { src: PropTypes.string.isRequired, alt: PropTypes.string, lang: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, sensitive: PropTypes.bool, currentTime: PropTypes.number, onOpenVideo: PropTypes.func, @@ -112,7 +110,6 @@ class Video extends React.PureComponent { inline: PropTypes.bool, editable: PropTypes.bool, alwaysVisible: PropTypes.bool, - cacheWidth: PropTypes.func, visible: PropTypes.bool, letterbox: PropTypes.bool, fullwidth: PropTypes.bool, @@ -138,41 +135,16 @@ class Video extends React.PureComponent { volume: 0.5, paused: true, dragging: false, - containerWidth: this.props.width, fullscreen: false, hovered: false, muted: false, revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'), }; - componentWillReceiveProps (nextProps) { - if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { - this.setState({ revealed: nextProps.visible }); - } - } - setPlayerRef = c => { this.player = c; - - if (this.player) { - this._setDimensions(); - } }; - _setDimensions () { - const width = this.player.offsetWidth; - - if (width && width !== this.state.containerWidth) { - if (this.props.cacheWidth) { - this.props.cacheWidth(width); - } - - this.setState({ - containerWidth: width, - }); - } - } - setVideoRef = c => { this.video = c; @@ -381,12 +353,10 @@ class Video extends React.PureComponent { document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); window.addEventListener('scroll', this.handleScroll); - window.addEventListener('resize', this.handleResize, { passive: true }); } componentWillUnmount () { window.removeEventListener('scroll', this.handleScroll); - window.removeEventListener('resize', this.handleResize); document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true); document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); @@ -403,26 +373,18 @@ class Video extends React.PureComponent { } } - componentDidUpdate (prevProps) { - if (this.player && this.player.offsetWidth && this.player.offsetWidth !== this.state.containerWidth && !this.state.fullscreen) { - if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth); - this.setState({ - containerWidth: this.player.offsetWidth, - }); + componentWillReceiveProps (nextProps) { + if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { + this.setState({ revealed: nextProps.visible }); } + } + + componentDidUpdate (prevProps) { if (this.video && this.state.revealed && this.props.preventPlayback && !prevProps.preventPlayback) { this.video.pause(); } } - handleResize = debounce(() => { - if (this.player) { - this._setDimensions(); - } - }, 250, { - trailing: true, - }); - handleScroll = throttle(() => { if (!this.video) { return; @@ -540,21 +502,12 @@ class Video extends React.PureComponent { render () { const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus } = this.props; - const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; + const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const progress = Math.min((currentTime / duration) * 100, 100); const playerStyle = {}; - const computedClass = classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable, letterbox, 'full-width': fullwidth }); - - let { width, height } = this.props; - - if (inline && containerWidth) { - width = containerWidth; - height = containerWidth / (16/9); - - playerStyle.height = height; - } else if (inline) { - return (<div className={computedClass} ref={this.setPlayerRef} tabIndex={0} />); + if (inline) { + playerStyle.aspectRatio = '16 / 9'; } let preload; @@ -578,7 +531,7 @@ class Video extends React.PureComponent { return ( <div role='menuitem' - className={computedClass} + className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable, letterbox, 'full-width': fullwidth })} style={playerStyle} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} @@ -605,8 +558,6 @@ class Video extends React.PureComponent { aria-label={alt} title={alt} lang={lang} - width={width} - height={height} volume={volume} onClick={this.togglePlay} onKeyDown={this.handleVideoKeyDown} @@ -615,6 +566,7 @@ class Video extends React.PureComponent { onLoadedData={this.handleLoadedData} onProgress={this.handleProgress} onVolumeChange={this.handleVolumeChange} + style={{ ...playerStyle, width: '100%' }} />} <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}> diff --git a/app/javascript/flavours/glitch/is_mobile.js b/app/javascript/flavours/glitch/is_mobile.ts similarity index 72% rename from app/javascript/flavours/glitch/is_mobile.js rename to app/javascript/flavours/glitch/is_mobile.ts index f87da2c5de..d33246882d 100644 --- a/app/javascript/flavours/glitch/is_mobile.js +++ b/app/javascript/flavours/glitch/is_mobile.ts @@ -1,21 +1,12 @@ -// @ts-check - import { supportsPassiveEvents } from 'detect-passive-events'; import { forceSingleColumn } from 'flavours/glitch/initial_state'; const LAYOUT_BREAKPOINT = 630; -/** - * @param {number} width - * @returns {boolean} - */ -export const isMobile = width => width <= LAYOUT_BREAKPOINT; +export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT; -/** - * @param {string} layout_local_setting - * @returns {string} - */ -export const layoutFromWindow = (layout_local_setting) => { +export type LayoutType = 'mobile' | 'single-column' | 'multi-column'; +export const layoutFromWindow = (layout_local_setting : string): LayoutType => { switch (layout_local_setting) { case 'multiple': return 'multi-column'; @@ -36,8 +27,9 @@ export const layoutFromWindow = (layout_local_setting) => { } }; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error -const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; +const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && window.MSStream != null; const listenerOptions = supportsPassiveEvents ? { passive: true } : false; diff --git a/app/javascript/flavours/glitch/main.jsx b/app/javascript/flavours/glitch/main.jsx index 14a6effbbb..e3a98b4843 100644 --- a/app/javascript/flavours/glitch/main.jsx +++ b/app/javascript/flavours/glitch/main.jsx @@ -1,7 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications'; -import Mastodon, { store } from 'flavours/glitch/containers/mastodon'; +import Mastodon from 'flavours/glitch/containers/mastodon'; +import { store } from 'flavours/glitch/store/configureStore'; import { me } from 'flavours/glitch/initial_state'; import ready from 'flavours/glitch/ready'; diff --git a/app/javascript/flavours/glitch/permissions.js b/app/javascript/flavours/glitch/permissions.ts similarity index 100% rename from app/javascript/flavours/glitch/permissions.js rename to app/javascript/flavours/glitch/permissions.ts diff --git a/app/javascript/flavours/glitch/reducers/meta.js b/app/javascript/flavours/glitch/reducers/meta.js index 7a38a9090c..c38dc60965 100644 --- a/app/javascript/flavours/glitch/reducers/meta.js +++ b/app/javascript/flavours/glitch/reducers/meta.js @@ -1,5 +1,5 @@ import { STORE_HYDRATE } from 'flavours/glitch/actions/store'; -import { APP_LAYOUT_CHANGE } from 'flavours/glitch/actions/app'; +import { changeLayout } from 'flavours/glitch/actions/app'; import { Map as ImmutableMap } from 'immutable'; import { layoutFromWindow } from 'flavours/glitch/is_mobile'; @@ -16,8 +16,8 @@ export default function meta(state = initialState, action) { return state.merge(action.state.get('meta')) .set('permissions', action.state.getIn(['role', 'permissions'])) .set('layout', layoutFromWindow(action.state.getIn(['local_settings', 'layout']))); - case APP_LAYOUT_CHANGE: - return state.set('layout', action.layout); + case changeLayout.type: + return state.set('layout', action.payload.layout); default: return state; } diff --git a/app/javascript/mastodon/scroll.js b/app/javascript/flavours/glitch/scroll.ts similarity index 51% rename from app/javascript/mastodon/scroll.js rename to app/javascript/flavours/glitch/scroll.ts index 84fe582699..1e509c4175 100644 --- a/app/javascript/mastodon/scroll.js +++ b/app/javascript/flavours/glitch/scroll.ts @@ -1,6 +1,5 @@ -const easingOutQuint = (x, t, b, c, d) => c * ((t = t / d - 1) * t * t * t * t + 1) + b; - -const scroll = (node, key, target) => { +const easingOutQuint = (x: number, t: number, b: number, c: number, d: number) => c * ((t = t / d - 1) * t * t * t * t + 1) + b; +const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) => { const startTime = Date.now(); const offset = node[key]; const gap = target - offset; @@ -28,5 +27,5 @@ const scroll = (node, key, target) => { const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style; -export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position); -export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0); +export const scrollRight = (node: Element, position: number) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position); +export const scrollTop = (node: Element) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0); diff --git a/app/javascript/flavours/glitch/store/configureStore.js b/app/javascript/flavours/glitch/store/configureStore.js index 0e0d45c668..cb17dd9ce8 100644 --- a/app/javascript/flavours/glitch/store/configureStore.js +++ b/app/javascript/flavours/glitch/store/configureStore.js @@ -1,15 +1,16 @@ -import { createStore, applyMiddleware, compose } from 'redux'; +import { configureStore } from '@reduxjs/toolkit'; import thunk from 'redux-thunk'; import appReducer from '../reducers'; import loadingBarMiddleware from '../middleware/loading_bar'; import errorsMiddleware from '../middleware/errors'; import soundsMiddleware from '../middleware/sounds'; -export default function configureStore() { - return createStore(appReducer, compose(applyMiddleware( +export const store = configureStore({ + reducer: appReducer, + middleware: [ thunk, loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), errorsMiddleware(), soundsMiddleware(), - ), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f)); -} + ], +}); diff --git a/app/javascript/flavours/glitch/styles/_mixins.scss b/app/javascript/flavours/glitch/styles/_mixins.scss index b23c4dbb7b..6643cd1aa5 100644 --- a/app/javascript/flavours/glitch/styles/_mixins.scss +++ b/app/javascript/flavours/glitch/styles/_mixins.scss @@ -47,7 +47,6 @@ margin-right: -14px; width: inherit; max-width: none; - height: 250px; border-radius: 0; } } diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index 2f36edd19b..a708d066ae 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -43,30 +43,25 @@ font-weight: 500; } -.media-gallery__gifv__label { - display: block; +.media-gallery__item__badges { position: absolute; - color: $primary-text-color; - background: rgba($base-overlay-background, 0.5); bottom: 6px; inset-inline-start: 6px; - padding: 2px 6px; - border-radius: 2px; - font-size: 11px; - font-weight: 600; - z-index: 1; - pointer-events: none; - opacity: 0.9; - transition: opacity 0.1s ease; - line-height: 18px; + display: flex; + gap: 2px; } -.media-gallery__gifv { - &:hover { - .media-gallery__gifv__label { - opacity: 1; - } - } +.media-gallery__gifv__label { + display: block; + color: $white; + background: rgba($black, 0.65); + padding: 2px 6px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; + z-index: 1; + pointer-events: none; + line-height: 18px; } .media-gallery { @@ -77,6 +72,10 @@ position: relative; width: 100%; min-height: 64px; + display: grid; + grid-template-columns: 50% 50%; + grid-template-rows: 50% 50%; + gap: 2px; @include fullwidth-gallery; } @@ -85,13 +84,16 @@ border: 0; box-sizing: border-box; display: block; - float: left; position: relative; border-radius: 4px; overflow: hidden; - .full-width & { - border-radius: 0; + &--tall { + grid-row: span 2; + } + + &--wide { + grid-column: span 2; } &.standalone { @@ -101,6 +103,10 @@ } } + .full-width & { + border-radius: 0; + } + &.letterbox { background: $base-shadow-color; } diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index 558f7cd451..ddf691dddf 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -691,7 +691,6 @@ a.status__display-name, margin-inline-end: 10px; height: 48px; width: 48px; - box-shadow: 0 0 0 2px $ui-base-color; } .muted { @@ -809,6 +808,10 @@ a.status-card { } .status-card-video { + // Firefox has a bug where frameborder=0 iframes add some extra blank space + // see https://bugzilla.mozilla.org/show_bug.cgi?id=155174 + overflow: hidden; + iframe { width: 100%; height: 100%; @@ -1154,6 +1157,7 @@ a.status-card.compact:hover { font-weight: 500; cursor: pointer; color: $darker-text-color; + aspect-ratio: 16 / 9; i { display: block; diff --git a/app/javascript/flavours/glitch/types/resources.ts b/app/javascript/flavours/glitch/types/resources.ts new file mode 100644 index 0000000000..28fad2719a --- /dev/null +++ b/app/javascript/flavours/glitch/types/resources.ts @@ -0,0 +1,10 @@ +import type { Record } from 'immutable'; + +type AccountValues = { + id: number; + avatar: string; + avatar_static: string; + [key: string]: any; +}; + +export type Account = Record<AccountValues>; diff --git a/app/javascript/flavours/glitch/types/util.ts b/app/javascript/flavours/glitch/types/util.ts new file mode 100644 index 0000000000..5f2cf2cf07 --- /dev/null +++ b/app/javascript/flavours/glitch/types/util.ts @@ -0,0 +1 @@ +export type ValueOf<T> = T[keyof T]; diff --git a/app/javascript/flavours/glitch/utils/base64.js b/app/javascript/flavours/glitch/utils/base64.ts similarity index 79% rename from app/javascript/flavours/glitch/utils/base64.js rename to app/javascript/flavours/glitch/utils/base64.ts index 8226e2c54e..5a595ee12b 100644 --- a/app/javascript/flavours/glitch/utils/base64.js +++ b/app/javascript/flavours/glitch/utils/base64.ts @@ -1,4 +1,4 @@ -export const decode = base64 => { +export const decode = (base64: string): Uint8Array => { const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); diff --git a/app/javascript/mastodon/utils/filters.js b/app/javascript/flavours/glitch/utils/filters.ts similarity index 83% rename from app/javascript/mastodon/utils/filters.js rename to app/javascript/flavours/glitch/utils/filters.ts index 97b433a991..5af2aa96a4 100644 --- a/app/javascript/mastodon/utils/filters.js +++ b/app/javascript/flavours/glitch/utils/filters.ts @@ -1,4 +1,4 @@ -export const toServerSideType = columnType => { +export const toServerSideType = (columnType: string) => { switch (columnType) { case 'home': case 'notifications': diff --git a/app/javascript/flavours/glitch/utils/numbers.js b/app/javascript/flavours/glitch/utils/numbers.ts similarity index 71% rename from app/javascript/flavours/glitch/utils/numbers.js rename to app/javascript/flavours/glitch/utils/numbers.ts index fa3d58fad1..128ba72f9b 100644 --- a/app/javascript/flavours/glitch/utils/numbers.js +++ b/app/javascript/flavours/glitch/utils/numbers.ts @@ -1,23 +1,19 @@ -// @ts-check +import type { ValueOf } from 'flavours/glitch/types/util'; export const DECIMAL_UNITS = Object.freeze({ ONE: 1, TEN: 10, - HUNDRED: Math.pow(10, 2), - THOUSAND: Math.pow(10, 3), - MILLION: Math.pow(10, 6), - BILLION: Math.pow(10, 9), - TRILLION: Math.pow(10, 12), + HUNDRED: 100, + THOUSAND: 1_000, + MILLION: 1_000_000, + BILLION: 1_000_000_000, + TRILLION: 1_000_000_000_000, }); +export type DecimalUnits = ValueOf<typeof DECIMAL_UNITS>; const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10; const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10; -/** - * @typedef {[number, number, number]} ShortNumber - * Array of: shorten number, unit of shorten number and maximum fraction digits - */ - /** * @param {number} sourceNumber Number to convert to short number * @returns {ShortNumber} Calculated short number @@ -25,7 +21,8 @@ const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10; * shortNumber(5936); * // => [5.936, 1000, 1] */ -export function toShortNumber(sourceNumber) { +export type ShortNumber = [number, DecimalUnits, 0 | 1] // Array of: shorten number, unit of shorten number and maximum fraction digits +export function toShortNumber(sourceNumber: number): ShortNumber { if (sourceNumber < DECIMAL_UNITS.THOUSAND) { return [sourceNumber, DECIMAL_UNITS.ONE, 0]; } else if (sourceNumber < DECIMAL_UNITS.MILLION) { @@ -59,20 +56,16 @@ export function toShortNumber(sourceNumber) { * pluralReady(1793, DECIMAL_UNITS.THOUSAND) * // => 1790 */ -export function pluralReady(sourceNumber, division) { +export function pluralReady(sourceNumber: number, division: DecimalUnits): number { if (division == null || division < DECIMAL_UNITS.HUNDRED) { return sourceNumber; } - let closestScale = division / DECIMAL_UNITS.TEN; + const closestScale = division / DECIMAL_UNITS.TEN; return Math.trunc(sourceNumber / closestScale) * closestScale; } -/** - * @param {number} num - * @returns {number} - */ -export function roundTo10(num) { +export function roundTo10(num: number): number { return Math.round(num * 0.1) / 0.1; } diff --git a/app/javascript/mastodon/actions/app.js b/app/javascript/mastodon/actions/app.js deleted file mode 100644 index c817c87080..0000000000 --- a/app/javascript/mastodon/actions/app.js +++ /dev/null @@ -1,17 +0,0 @@ -export const APP_FOCUS = 'APP_FOCUS'; -export const APP_UNFOCUS = 'APP_UNFOCUS'; - -export const focusApp = () => ({ - type: APP_FOCUS, -}); - -export const unfocusApp = () => ({ - type: APP_UNFOCUS, -}); - -export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE'; - -export const changeLayout = layout => ({ - type: APP_LAYOUT_CHANGE, - layout, -}); diff --git a/app/javascript/mastodon/actions/app.ts b/app/javascript/mastodon/actions/app.ts new file mode 100644 index 0000000000..0acfbfae7a --- /dev/null +++ b/app/javascript/mastodon/actions/app.ts @@ -0,0 +1,10 @@ +import { createAction } from '@reduxjs/toolkit'; + +export const focusApp = createAction('APP_FOCUS'); +export const unfocusApp = createAction('APP_UNFOCUS'); + +type ChangeLayoutPayload = { + layout: 'mobile' | 'single-column' | 'multi-column'; +}; +export const changeLayout = + createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE'); diff --git a/app/javascript/mastodon/blurhash.js b/app/javascript/mastodon/blurhash.ts similarity index 87% rename from app/javascript/mastodon/blurhash.js rename to app/javascript/mastodon/blurhash.ts index 5adcc3e770..cb1c3b2c82 100644 --- a/app/javascript/mastodon/blurhash.js +++ b/app/javascript/mastodon/blurhash.ts @@ -84,7 +84,7 @@ const DIGIT_CHARACTERS = [ '~', ]; -export const decode83 = (str) => { +export const decode83 = (str: string) => { let value = 0; let c, digit; @@ -97,13 +97,13 @@ export const decode83 = (str) => { return value; }; -export const intToRGB = int => ({ +export const intToRGB = (int: number) => ({ r: Math.max(0, (int >> 16)), g: Math.max(0, (int >> 8) & 255), b: Math.max(0, (int & 255)), }); -export const getAverageFromBlurhash = blurhash => { +export const getAverageFromBlurhash = (blurhash: string) => { if (!blurhash) { return null; } diff --git a/app/javascript/flavours/glitch/compare_id.js b/app/javascript/mastodon/compare_id.ts similarity index 72% rename from app/javascript/flavours/glitch/compare_id.js rename to app/javascript/mastodon/compare_id.ts index d2bd74f447..ae4ac6f897 100644 --- a/app/javascript/flavours/glitch/compare_id.js +++ b/app/javascript/mastodon/compare_id.ts @@ -1,4 +1,4 @@ -export default function compareId (id1, id2) { +export default function compareId (id1: string, id2: string) { if (id1 === id2) { return 0; } diff --git a/app/javascript/mastodon/components/blurhash.jsx b/app/javascript/mastodon/components/blurhash.jsx deleted file mode 100644 index f5c58e04ef..0000000000 --- a/app/javascript/mastodon/components/blurhash.jsx +++ /dev/null @@ -1,65 +0,0 @@ -// @ts-check - -import { decode } from 'blurhash'; -import React, { useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; - -/** - * @typedef BlurhashPropsBase - * @property {string?} hash Hash to render - * @property {number} width - * Width of the blurred region in pixels. Defaults to 32 - * @property {number} [height] - * Height of the blurred region in pixels. Defaults to width - * @property {boolean} [dummy] - * Whether dummy mode is enabled. If enabled, nothing is rendered - * and canvas left untouched - */ - -/** @typedef {JSX.IntrinsicElements['canvas'] & BlurhashPropsBase} BlurhashProps */ - -/** - * Component that is used to render blurred of blurhash string - * @param {BlurhashProps} param1 Props of the component - * @returns {JSX.Element} Canvas which will render blurred region element to embed - */ -function Blurhash({ - hash, - width = 32, - height = width, - dummy = false, - ...canvasProps -}) { - const canvasRef = /** @type {import('react').MutableRefObject<HTMLCanvasElement>} */ (useRef()); - - useEffect(() => { - const { current: canvas } = canvasRef; - canvas.width = canvas.width; // resets canvas - - if (dummy || !hash) return; - - try { - const pixels = decode(hash, width, height); - const ctx = canvas.getContext('2d'); - const imageData = new ImageData(pixels, width, height); - - // @ts-expect-error - ctx.putImageData(imageData, 0, 0); - } catch (err) { - console.error('Blurhash decoding failure', { err, hash }); - } - }, [dummy, hash, width, height]); - - return ( - <canvas {...canvasProps} ref={canvasRef} width={width} height={height} /> - ); -} - -Blurhash.propTypes = { - hash: PropTypes.string.isRequired, - width: PropTypes.number, - height: PropTypes.number, - dummy: PropTypes.bool, -}; - -export default React.memo(Blurhash); diff --git a/app/javascript/mastodon/components/blurhash.tsx b/app/javascript/mastodon/components/blurhash.tsx new file mode 100644 index 0000000000..6fec6e1ef7 --- /dev/null +++ b/app/javascript/mastodon/components/blurhash.tsx @@ -0,0 +1,45 @@ +import { decode } from 'blurhash'; +import React, { useRef, useEffect } from 'react'; + +type Props = { + hash: string; + width?: number; + height?: number; + dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched + children?: never; + [key: string]: any; +} +function Blurhash({ + hash, + width = 32, + height = width, + dummy = false, + ...canvasProps +}: Props) { + const canvasRef = useRef<HTMLCanvasElement>(null); + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const canvas = canvasRef.current!; + // eslint-disable-next-line no-self-assign + canvas.width = canvas.width; // resets canvas + + if (dummy || !hash) return; + + try { + const pixels = decode(hash, width, height); + const ctx = canvas.getContext('2d'); + const imageData = new ImageData(pixels, width, height); + + ctx?.putImageData(imageData, 0, 0); + } catch (err) { + console.error('Blurhash decoding failure', { err, hash }); + } + }, [dummy, hash, width, height]); + + return ( + <canvas {...canvasProps} ref={canvasRef} width={width} height={height} /> + ); +} + +export default React.memo(Blurhash); diff --git a/app/javascript/mastodon/components/column_back_button.jsx b/app/javascript/mastodon/components/column_back_button.jsx index 12926bb253..faa800b2ad 100644 --- a/app/javascript/mastodon/components/column_back_button.jsx +++ b/app/javascript/mastodon/components/column_back_button.jsx @@ -21,7 +21,9 @@ export default class ColumnBackButton extends React.PureComponent { if (onClick) { onClick(); - } else if (window.history && window.history.state) { + // Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201 + // When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location + } else if (router.route.location.key) { router.history.goBack(); } else { router.history.push('/'); diff --git a/app/javascript/mastodon/components/icon_button.jsx b/app/javascript/mastodon/components/icon_button.tsx similarity index 68% rename from app/javascript/mastodon/components/icon_button.jsx rename to app/javascript/mastodon/components/icon_button.tsx index 989cae4401..ec11ab7011 100644 --- a/app/javascript/mastodon/components/icon_button.jsx +++ b/app/javascript/mastodon/components/icon_button.tsx @@ -1,34 +1,36 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; -import Icon from 'mastodon/components/icon'; -import AnimatedNumber from 'mastodon/components/animated_number'; +import { Icon } from './icon'; +import { AnimatedNumber } from './animated_number'; -export default class IconButton extends React.PureComponent { - - static propTypes = { - className: PropTypes.string, - title: PropTypes.string.isRequired, - icon: PropTypes.string.isRequired, - onClick: PropTypes.func, - onMouseDown: PropTypes.func, - onKeyDown: PropTypes.func, - onKeyPress: PropTypes.func, - size: PropTypes.number, - active: PropTypes.bool, - expanded: PropTypes.bool, - style: PropTypes.object, - activeStyle: PropTypes.object, - disabled: PropTypes.bool, - inverted: PropTypes.bool, - animate: PropTypes.bool, - overlay: PropTypes.bool, - tabIndex: PropTypes.number, - counter: PropTypes.number, - obfuscateCount: PropTypes.bool, - href: PropTypes.string, - ariaHidden: PropTypes.bool, - }; +type Props = { + className?: string; + title: string; + icon: string; + onClick?: React.MouseEventHandler<HTMLButtonElement>; + onMouseDown?: React.MouseEventHandler<HTMLButtonElement>; + onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>; + onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>; + size: number; + active: boolean; + expanded?: boolean; + style?: React.CSSProperties; + activeStyle?: React.CSSProperties; + disabled: boolean; + inverted?: boolean; + animate: boolean; + overlay: boolean; + tabIndex: number; + counter?: number; + obfuscateCount?: boolean; + href?: string; + ariaHidden: boolean; +} +type States = { + activate: boolean, + deactivate: boolean, +} +export default class IconButton extends React.PureComponent<Props, States> { static defaultProps = { size: 18, @@ -45,7 +47,7 @@ export default class IconButton extends React.PureComponent { deactivate: false, }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps: Props) { if (!nextProps.animate) return; if (this.props.active && !nextProps.active) { @@ -55,27 +57,27 @@ export default class IconButton extends React.PureComponent { } } - handleClick = (e) => { + handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => { e.preventDefault(); - if (!this.props.disabled) { + if (!this.props.disabled && this.props.onClick != null) { this.props.onClick(e); } }; - handleKeyPress = (e) => { + handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> = (e) => { if (this.props.onKeyPress && !this.props.disabled) { this.props.onKeyPress(e); } }; - handleMouseDown = (e) => { + handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (e) => { if (!this.props.disabled && this.props.onMouseDown) { this.props.onMouseDown(e); } }; - handleKeyDown = (e) => { + handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (e) => { if (!this.props.disabled && this.props.onKeyDown) { this.props.onKeyDown(e); } @@ -132,7 +134,7 @@ export default class IconButton extends React.PureComponent { </React.Fragment> ); - if (href && !this.prop) { + if (href != null) { contents = ( <a href={href} target='_blank' rel='noopener noreferrer'> {contents} diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx index 5be0070a33..54470f9402 100644 --- a/app/javascript/mastodon/components/media_gallery.jsx +++ b/app/javascript/mastodon/components/media_gallery.jsx @@ -81,12 +81,10 @@ class Item extends React.PureComponent { render () { const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props; + let badges = [], thumbnail; + let width = 50; let height = 100; - let top = 'auto'; - let left = 'auto'; - let bottom = 'auto'; - let right = 'auto'; if (size === 1) { width = 100; @@ -96,45 +94,13 @@ class Item extends React.PureComponent { height = 50; } - if (size === 2) { - if (index === 0) { - right = '2px'; - } else { - left = '2px'; - } - } else if (size === 3) { - if (index === 0) { - right = '2px'; - } else if (index > 0) { - left = '2px'; - } - - if (index === 1) { - bottom = '2px'; - } else if (index > 1) { - top = '2px'; - } - } else if (size === 4) { - if (index === 0 || index === 2) { - right = '2px'; - } - - if (index === 1 || index === 3) { - left = '2px'; - } - - if (index < 2) { - bottom = '2px'; - } else { - top = '2px'; - } + if (attachment.get('description')?.length > 0) { + badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>); } - let thumbnail = ''; - if (attachment.get('type') === 'unknown') { return ( - <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> + <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}> <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'> <Blurhash hash={attachment.get('blurhash')} @@ -184,6 +150,8 @@ class Item extends React.PureComponent { } else if (attachment.get('type') === 'gifv') { const autoPlay = this.getAutoPlay(); + badges.push(<span key='gif' className='media-gallery__gifv__label'>GIF</span>); + thumbnail = ( <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> <video @@ -201,14 +169,12 @@ class Item extends React.PureComponent { loop muted /> - - <span className='media-gallery__gifv__label'>GIF</span> </div> ); } return ( - <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> + <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}> <Blurhash hash={attachment.get('blurhash')} dummy={!useBlurhash} @@ -216,7 +182,14 @@ class Item extends React.PureComponent { 'media-gallery__preview--hidden': visible && this.state.loaded, })} /> + {visible && thumbnail} + + {badges && ( + <div className='media-gallery__item__badges'> + {badges} + </div> + )} </div> ); } @@ -313,7 +286,7 @@ class MediaGallery extends React.PureComponent { } render () { - const { media, lang, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props; + const { media, lang, intl, sensitive, defaultWidth, standalone, autoplay } = this.props; const { visible } = this.state; const width = this.state.width || defaultWidth; @@ -322,13 +295,9 @@ class MediaGallery extends React.PureComponent { const style = {}; if (this.isFullSizeEligible() && (standalone || !cropImages)) { - if (width) { - style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); - } - } else if (width) { - style.height = width / (16/9); + style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`; } else { - style.height = height; + style.aspectRatio = '16 / 9'; } const size = media.take(4).size; diff --git a/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx b/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx index 6322b1c668..a51c974017 100644 --- a/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx +++ b/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx @@ -3,62 +3,22 @@ import PropTypes from 'prop-types'; import Icon from 'mastodon/components/icon'; import { removePictureInPicture } from 'mastodon/actions/picture_in_picture'; import { connect } from 'react-redux'; -import { debounce } from 'lodash'; import { FormattedMessage } from 'react-intl'; class PictureInPicturePlaceholder extends React.PureComponent { static propTypes = { - width: PropTypes.number, dispatch: PropTypes.func.isRequired, }; - state = { - width: this.props.width, - height: this.props.width && (this.props.width / (16/9)), - }; - handleClick = () => { const { dispatch } = this.props; dispatch(removePictureInPicture()); }; - setRef = c => { - this.node = c; - - if (this.node) { - this._setDimensions(); - } - }; - - _setDimensions () { - const width = this.node.offsetWidth; - const height = width / (16/9); - - this.setState({ width, height }); - } - - componentDidMount () { - window.addEventListener('resize', this.handleResize, { passive: true }); - } - - componentWillUnmount () { - window.removeEventListener('resize', this.handleResize); - } - - handleResize = debounce(() => { - if (this.node) { - this._setDimensions(); - } - }, 250, { - trailing: true, - }); - render () { - const { height } = this.state; - return ( - <div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex={0} onClick={this.handleClick}> + <div className='picture-in-picture-placeholder' role='button' tabIndex={0} onClick={this.handleClick}> <Icon id='window-restore' /> <FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' /> </div> diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index cd8423b2f4..b5242e7691 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -411,7 +411,7 @@ class Status extends ImmutablePureComponent { } if (pictureInPicture.get('inUse')) { - media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />; + media = <PictureInPicturePlaceholder />; } else if (status.get('media_attachments').size > 0) { if (this.props.muted) { media = ( @@ -460,12 +460,9 @@ class Status extends ImmutablePureComponent { src={attachment.get('url')} alt={attachment.get('description')} lang={status.get('language')} - width={this.props.cachedMediaWidth} - height={110} inline sensitive={status.get('sensitive')} onOpenVideo={this.handleOpenVideo} - cacheWidth={this.props.cacheMediaWidth} deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined} visible={this.state.showMedia} onToggleVisibility={this.handleToggleMediaVisibility} @@ -498,8 +495,6 @@ class Status extends ImmutablePureComponent { onOpenMedia={this.handleOpenMedia} card={status.get('card')} compact - cacheWidth={this.props.cacheMediaWidth} - defaultWidth={this.props.cachedMediaWidth} sensitive={status.get('sensitive')} /> ); diff --git a/app/javascript/mastodon/containers/compose_container.jsx b/app/javascript/mastodon/containers/compose_container.jsx index 7bc7bbaa4d..a4c5f3cb49 100644 --- a/app/javascript/mastodon/containers/compose_container.jsx +++ b/app/javascript/mastodon/containers/compose_container.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import PropTypes from 'prop-types'; -import configureStore from '../store/configureStore'; +import { store } from '../store/configureStore'; import { hydrateStore } from '../actions/store'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from '../locales'; @@ -12,8 +12,6 @@ import { fetchCustomEmojis } from '../actions/custom_emojis'; const { localeData, messages } = getLocale(); addLocaleData(localeData); -const store = configureStore(); - if (initialState) { store.dispatch(hydrateStore(initialState)); } diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index 002b71e93d..256ea8e2d9 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -5,7 +5,7 @@ import { IntlProvider, addLocaleData } from 'react-intl'; import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter, Route } from 'react-router-dom'; import { ScrollContext } from 'react-router-scroll-4'; -import configureStore from 'mastodon/store/configureStore'; +import { store } from 'mastodon/store/configureStore'; import UI from 'mastodon/features/ui'; import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis'; import { hydrateStore } from 'mastodon/actions/store'; @@ -19,7 +19,6 @@ addLocaleData(localeData); const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`; -export const store = configureStore(); const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); diff --git a/app/javascript/mastodon/features/audio/index.jsx b/app/javascript/mastodon/features/audio/index.jsx index 53f24c6a39..e8fe2c4d9a 100644 --- a/app/javascript/mastodon/features/audio/index.jsx +++ b/app/javascript/mastodon/features/audio/index.jsx @@ -384,7 +384,7 @@ class Audio extends React.PureComponent { } _getRadius () { - return parseInt(((this.state.height || this.props.height) - (PADDING * this._getScaleCoefficient()) * 2) / 2); + return parseInt((this.state.height || this.props.height) / 2 - PADDING * this._getScaleCoefficient()); } _getScaleCoefficient () { @@ -396,7 +396,7 @@ class Audio extends React.PureComponent { } _getCY() { - return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient())); + return Math.floor((this.state.height || this.props.height) / 2); } _getAccentColor () { @@ -470,7 +470,7 @@ class Audio extends React.PureComponent { } return ( - <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}> + <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}> <Blurhash hash={blurhash} @@ -515,9 +515,16 @@ class Audio extends React.PureComponent { {(revealed || editable) && <img src={this.props.poster} alt='' - width={(this._getRadius() - TICK_SIZE) * 2} - height={(this._getRadius() - TICK_SIZE) * 2} - style={{ position: 'absolute', left: this._getCX(), top: this._getCY(), transform: 'translate(-50%, -50%)', borderRadius: '50%', pointerEvents: 'none' }} + style={{ + position: 'absolute', + left: '50%', + top: '50%', + height: `calc(${(100 - 2 * 100 * PADDING / 982)}% - ${TICK_SIZE * 2}px)`, + aspectRatio: '1', + transform: 'translate(-50%, -50%)', + borderRadius: '50%', + pointerEvents: 'none', + }} />} <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> diff --git a/app/javascript/mastodon/features/status/components/card.jsx b/app/javascript/mastodon/features/status/components/card.jsx index b67f671c50..88b38c65ad 100644 --- a/app/javascript/mastodon/features/status/components/card.jsx +++ b/app/javascript/mastodon/features/status/components/card.jsx @@ -8,7 +8,6 @@ import classnames from 'classnames'; import Icon from 'mastodon/components/icon'; import { useBlurhash } from 'mastodon/initial_state'; import Blurhash from 'mastodon/components/blurhash'; -import { debounce } from 'lodash'; const IDNA_PREFIX = 'xn--'; @@ -54,8 +53,6 @@ export default class Card extends React.PureComponent { card: ImmutablePropTypes.map, onOpenMedia: PropTypes.func.isRequired, compact: PropTypes.bool, - defaultWidth: PropTypes.number, - cacheWidth: PropTypes.func, sensitive: PropTypes.bool, }; @@ -64,7 +61,6 @@ export default class Card extends React.PureComponent { }; state = { - width: this.props.defaultWidth || 280, previewLoaded: false, embedded: false, revealed: !this.props.sensitive, @@ -87,24 +83,6 @@ export default class Card extends React.PureComponent { window.removeEventListener('resize', this.handleResize); } - _setDimensions () { - const width = this.node.offsetWidth; - - if (this.props.cacheWidth) { - this.props.cacheWidth(width); - } - - this.setState({ width }); - } - - handleResize = debounce(() => { - if (this.node) { - this._setDimensions(); - } - }, 250, { - trailing: true, - }); - handlePhotoClick = () => { const { card, onOpenMedia } = this.props; @@ -138,10 +116,6 @@ export default class Card extends React.PureComponent { setRef = c => { this.node = c; - - if (this.node) { - this._setDimensions(); - } }; handleImageLoad = () => { @@ -157,36 +131,31 @@ export default class Card extends React.PureComponent { renderVideo () { const { card } = this.props; const content = { __html: addAutoPlay(card.get('html')) }; - const { width } = this.state; - const ratio = card.get('width') / card.get('height'); - const height = width / ratio; return ( <div ref={this.setRef} className='status-card__image status-card-video' dangerouslySetInnerHTML={content} - style={{ height }} + style={{ aspectRatio: `${card.get('width')} / ${card.get('height')}` }} /> ); } render () { const { card, compact } = this.props; - const { width, embedded, revealed } = this.state; + const { embedded, revealed } = this.state; if (card === null) { return null; } const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); - const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded; + const horizontal = (!compact && card.get('width') > card.get('height')) || card.get('type') !== 'link' || embedded; const interactive = card.get('type') !== 'link'; const className = classnames('status-card', { horizontal, compact, interactive }); const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; const language = card.get('language') || ''; - const ratio = card.get('width') / card.get('height'); - const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); const description = ( <div className='status-card__content' lang={language}> @@ -196,6 +165,14 @@ export default class Card extends React.PureComponent { </div> ); + const thumbnailStyle = { + visibility: revealed? null : 'hidden', + }; + + if (horizontal) { + thumbnailStyle.aspectRatio = (compact && !embedded) ? '16 / 9' : `${card.get('width')} / ${card.get('height')}`; + } + let embed = ''; let canvas = ( <Blurhash @@ -206,7 +183,7 @@ export default class Card extends React.PureComponent { dummy={!useBlurhash} /> ); - let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />; + let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />; let spoilerButton = ( <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'> <span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 2178687a9e..6dc5177b5c 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -362,7 +362,7 @@ class UI extends React.PureComponent { if (layout !== this.props.layout) { this.handleLayoutChange.cancel(); - this.props.dispatch(changeLayout(layout)); + this.props.dispatch(changeLayout({ layout })); } else { this.handleLayoutChange(); } diff --git a/app/javascript/mastodon/features/video/index.jsx b/app/javascript/mastodon/features/video/index.jsx index e2637e0bef..d76d34546b 100644 --- a/app/javascript/mastodon/features/video/index.jsx +++ b/app/javascript/mastodon/features/video/index.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { is } from 'immutable'; -import { throttle, debounce } from 'lodash'; +import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { displayMedia, useBlurhash } from '../../initial_state'; @@ -102,8 +102,6 @@ class Video extends React.PureComponent { src: PropTypes.string.isRequired, alt: PropTypes.string, lang: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, sensitive: PropTypes.bool, currentTime: PropTypes.number, onOpenVideo: PropTypes.func, @@ -112,7 +110,6 @@ class Video extends React.PureComponent { inline: PropTypes.bool, editable: PropTypes.bool, alwaysVisible: PropTypes.bool, - cacheWidth: PropTypes.func, visible: PropTypes.bool, onToggleVisibility: PropTypes.func, deployPictureInPicture: PropTypes.func, @@ -135,7 +132,6 @@ class Video extends React.PureComponent { volume: 0.5, paused: true, dragging: false, - containerWidth: this.props.width, fullscreen: false, hovered: false, muted: false, @@ -144,24 +140,8 @@ class Video extends React.PureComponent { setPlayerRef = c => { this.player = c; - - if (this.player) { - this._setDimensions(); - } }; - _setDimensions () { - const width = this.player.offsetWidth; - - if (this.props.cacheWidth) { - this.props.cacheWidth(width); - } - - this.setState({ - containerWidth: width, - }); - } - setVideoRef = c => { this.video = c; @@ -370,12 +350,10 @@ class Video extends React.PureComponent { document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); window.addEventListener('scroll', this.handleScroll); - window.addEventListener('resize', this.handleResize, { passive: true }); } componentWillUnmount () { window.removeEventListener('scroll', this.handleScroll); - window.removeEventListener('resize', this.handleResize); document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true); document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); @@ -404,14 +382,6 @@ class Video extends React.PureComponent { } } - handleResize = debounce(() => { - if (this.player) { - this._setDimensions(); - } - }, 250, { - trailing: true, - }); - handleScroll = throttle(() => { if (!this.video) { return; @@ -525,17 +495,12 @@ class Video extends React.PureComponent { render () { const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props; - const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; + const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const progress = Math.min((currentTime / duration) * 100, 100); const playerStyle = {}; - let { width, height } = this.props; - - if (inline && containerWidth) { - width = containerWidth; - height = containerWidth / (16/9); - - playerStyle.height = height; + if (inline) { + playerStyle.aspectRatio = '16 / 9'; } let preload; @@ -586,8 +551,6 @@ class Video extends React.PureComponent { aria-label={alt} title={alt} lang={lang} - width={width} - height={height} volume={volume} onClick={this.togglePlay} onKeyDown={this.handleVideoKeyDown} @@ -596,6 +559,7 @@ class Video extends React.PureComponent { onLoadedData={this.handleLoadedData} onProgress={this.handleProgress} onVolumeChange={this.handleVolumeChange} + style={{ ...playerStyle, width: '100%' }} />} <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}> diff --git a/app/javascript/mastodon/is_mobile.js b/app/javascript/mastodon/is_mobile.ts similarity index 62% rename from app/javascript/mastodon/is_mobile.js rename to app/javascript/mastodon/is_mobile.ts index d0669b74bf..43819f85db 100644 --- a/app/javascript/mastodon/is_mobile.js +++ b/app/javascript/mastodon/is_mobile.ts @@ -1,21 +1,12 @@ -// @ts-check - import { supportsPassiveEvents } from 'detect-passive-events'; -// @ts-expect-error -import { forceSingleColumn } from 'mastodon/initial_state'; +import { forceSingleColumn } from './initial_state'; const LAYOUT_BREAKPOINT = 630; -/** - * @param {number} width - * @returns {boolean} - */ -export const isMobile = width => width <= LAYOUT_BREAKPOINT; +export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT; -/** - * @returns {string} - */ -export const layoutFromWindow = () => { +export type LayoutType = 'mobile' | 'single-column' | 'multi-column'; +export const layoutFromWindow = (): LayoutType => { if (isMobile(window.innerWidth)) { return 'mobile'; } else if (forceSingleColumn) { @@ -25,8 +16,9 @@ export const layoutFromWindow = () => { } }; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error -const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; +const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && window.MSStream != null; const listenerOptions = supportsPassiveEvents ? { passive: true } : false; diff --git a/app/javascript/mastodon/main.jsx b/app/javascript/mastodon/main.jsx index 69a7ee91f9..88a205dd24 100644 --- a/app/javascript/mastodon/main.jsx +++ b/app/javascript/mastodon/main.jsx @@ -1,7 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { setupBrowserNotifications } from 'mastodon/actions/notifications'; -import Mastodon, { store } from 'mastodon/containers/mastodon'; +import Mastodon from 'mastodon/containers/mastodon'; +import { store } from 'mastodon/store/configureStore'; import { me } from 'mastodon/initial_state'; import ready from 'mastodon/ready'; diff --git a/app/javascript/mastodon/permissions.js b/app/javascript/mastodon/permissions.ts similarity index 100% rename from app/javascript/mastodon/permissions.js rename to app/javascript/mastodon/permissions.ts diff --git a/app/javascript/mastodon/reducers/meta.js b/app/javascript/mastodon/reducers/meta.js index 755dd73905..16ce751aad 100644 --- a/app/javascript/mastodon/reducers/meta.js +++ b/app/javascript/mastodon/reducers/meta.js @@ -1,5 +1,5 @@ import { STORE_HYDRATE } from 'mastodon/actions/store'; -import { APP_LAYOUT_CHANGE } from 'mastodon/actions/app'; +import { changeLayout } from 'mastodon/actions/app'; import { Map as ImmutableMap } from 'immutable'; import { layoutFromWindow } from 'mastodon/is_mobile'; @@ -14,8 +14,8 @@ export default function meta(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: return state.merge(action.state.get('meta')).set('permissions', action.state.getIn(['role', 'permissions'])); - case APP_LAYOUT_CHANGE: - return state.set('layout', action.layout); + case changeLayout.type: + return state.set('layout', action.payload.layout); default: return state; } diff --git a/app/javascript/mastodon/reducers/missed_updates.js b/app/javascript/mastodon/reducers/missed_updates.js deleted file mode 100644 index a3141d854e..0000000000 --- a/app/javascript/mastodon/reducers/missed_updates.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; -import { NOTIFICATIONS_UPDATE } from 'mastodon/actions/notifications'; -import { APP_FOCUS, APP_UNFOCUS } from 'mastodon/actions/app'; - -const initialState = ImmutableMap({ - focused: true, - unread: 0, -}); - -export default function missed_updates(state = initialState, action) { - switch(action.type) { - case APP_FOCUS: - return state.set('focused', true).set('unread', 0); - case APP_UNFOCUS: - return state.set('focused', false); - case NOTIFICATIONS_UPDATE: - return state.get('focused') ? state : state.update('unread', x => x + 1); - default: - return state; - } -} diff --git a/app/javascript/mastodon/reducers/missed_updates.ts b/app/javascript/mastodon/reducers/missed_updates.ts new file mode 100644 index 0000000000..043fe93faf --- /dev/null +++ b/app/javascript/mastodon/reducers/missed_updates.ts @@ -0,0 +1,31 @@ +import { Record } from 'immutable'; +import type { Action } from 'redux'; +import { NOTIFICATIONS_UPDATE } from '../actions/notifications'; +import { focusApp, unfocusApp } from '../actions/app'; + +type MissedUpdatesState = { + focused: boolean; + unread: number; +}; +const initialState = Record<MissedUpdatesState>({ + focused: true, + unread: 0, +})(); + +export default function missed_updates( + state = initialState, + action: Action<string>, +) { + switch (action.type) { + case focusApp.type: + return state.set('focused', true).set('unread', 0); + case unfocusApp.type: + return state.set('focused', false); + case NOTIFICATIONS_UPDATE: + return state.get('focused') + ? state + : state.update('unread', (x) => x + 1); + default: + return state; + } +} diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 44fa1c6134..0530a52b4c 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -23,8 +23,8 @@ import { MARKERS_FETCH_SUCCESS, } from '../actions/markers'; import { - APP_FOCUS, - APP_UNFOCUS, + focusApp, + unfocusApp, } from '../actions/app'; import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks'; import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'; @@ -258,9 +258,9 @@ export default function notifications(state = initialState, action) { return updateMounted(state); case NOTIFICATIONS_UNMOUNT: return state.update('mounted', count => count - 1); - case APP_FOCUS: + case focusApp.type: return updateVisibility(state, true); - case APP_UNFOCUS: + case unfocusApp.type: return updateVisibility(state, false); case NOTIFICATIONS_LOAD_PENDING: return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0); diff --git a/app/javascript/flavours/glitch/scroll.js b/app/javascript/mastodon/scroll.ts similarity index 51% rename from app/javascript/flavours/glitch/scroll.js rename to app/javascript/mastodon/scroll.ts index 84fe582699..1e509c4175 100644 --- a/app/javascript/flavours/glitch/scroll.js +++ b/app/javascript/mastodon/scroll.ts @@ -1,6 +1,5 @@ -const easingOutQuint = (x, t, b, c, d) => c * ((t = t / d - 1) * t * t * t * t + 1) + b; - -const scroll = (node, key, target) => { +const easingOutQuint = (x: number, t: number, b: number, c: number, d: number) => c * ((t = t / d - 1) * t * t * t * t + 1) + b; +const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) => { const startTime = Date.now(); const offset = node[key]; const gap = target - offset; @@ -28,5 +27,5 @@ const scroll = (node, key, target) => { const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style; -export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position); -export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0); +export const scrollRight = (node: Element, position: number) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position); +export const scrollTop = (node: Element) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0); diff --git a/app/javascript/mastodon/store/configureStore.js b/app/javascript/mastodon/store/configureStore.js index 0e0d45c668..cb17dd9ce8 100644 --- a/app/javascript/mastodon/store/configureStore.js +++ b/app/javascript/mastodon/store/configureStore.js @@ -1,15 +1,16 @@ -import { createStore, applyMiddleware, compose } from 'redux'; +import { configureStore } from '@reduxjs/toolkit'; import thunk from 'redux-thunk'; import appReducer from '../reducers'; import loadingBarMiddleware from '../middleware/loading_bar'; import errorsMiddleware from '../middleware/errors'; import soundsMiddleware from '../middleware/sounds'; -export default function configureStore() { - return createStore(appReducer, compose(applyMiddleware( +export const store = configureStore({ + reducer: appReducer, + middleware: [ thunk, loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), errorsMiddleware(), soundsMiddleware(), - ), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f)); -} + ], +}); diff --git a/app/javascript/mastodon/utils/base64.js b/app/javascript/mastodon/utils/base64.ts similarity index 79% rename from app/javascript/mastodon/utils/base64.js rename to app/javascript/mastodon/utils/base64.ts index 8226e2c54e..5a595ee12b 100644 --- a/app/javascript/mastodon/utils/base64.js +++ b/app/javascript/mastodon/utils/base64.ts @@ -1,4 +1,4 @@ -export const decode = base64 => { +export const decode = (base64: string): Uint8Array => { const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); diff --git a/app/javascript/flavours/glitch/utils/filters.js b/app/javascript/mastodon/utils/filters.ts similarity index 83% rename from app/javascript/flavours/glitch/utils/filters.js rename to app/javascript/mastodon/utils/filters.ts index 97b433a991..5af2aa96a4 100644 --- a/app/javascript/flavours/glitch/utils/filters.js +++ b/app/javascript/mastodon/utils/filters.ts @@ -1,4 +1,4 @@ -export const toServerSideType = columnType => { +export const toServerSideType = (columnType: string) => { switch (columnType) { case 'home': case 'notifications': diff --git a/app/javascript/mastodon/utils/hashtags.js b/app/javascript/mastodon/utils/hashtags.ts similarity index 100% rename from app/javascript/mastodon/utils/hashtags.js rename to app/javascript/mastodon/utils/hashtags.ts diff --git a/app/javascript/mastodon/utils/numbers.js b/app/javascript/mastodon/utils/numbers.ts similarity index 71% rename from app/javascript/mastodon/utils/numbers.js rename to app/javascript/mastodon/utils/numbers.ts index fa3d58fad1..35af8a973c 100644 --- a/app/javascript/mastodon/utils/numbers.js +++ b/app/javascript/mastodon/utils/numbers.ts @@ -1,23 +1,19 @@ -// @ts-check +import type { ValueOf } from '../../types/util'; export const DECIMAL_UNITS = Object.freeze({ ONE: 1, TEN: 10, - HUNDRED: Math.pow(10, 2), - THOUSAND: Math.pow(10, 3), - MILLION: Math.pow(10, 6), - BILLION: Math.pow(10, 9), - TRILLION: Math.pow(10, 12), + HUNDRED: 100, + THOUSAND: 1_000, + MILLION: 1_000_000, + BILLION: 1_000_000_000, + TRILLION: 1_000_000_000_000, }); +export type DecimalUnits = ValueOf<typeof DECIMAL_UNITS>; const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10; const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10; -/** - * @typedef {[number, number, number]} ShortNumber - * Array of: shorten number, unit of shorten number and maximum fraction digits - */ - /** * @param {number} sourceNumber Number to convert to short number * @returns {ShortNumber} Calculated short number @@ -25,7 +21,8 @@ const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10; * shortNumber(5936); * // => [5.936, 1000, 1] */ -export function toShortNumber(sourceNumber) { +export type ShortNumber = [number, DecimalUnits, 0 | 1] // Array of: shorten number, unit of shorten number and maximum fraction digits +export function toShortNumber(sourceNumber: number): ShortNumber { if (sourceNumber < DECIMAL_UNITS.THOUSAND) { return [sourceNumber, DECIMAL_UNITS.ONE, 0]; } else if (sourceNumber < DECIMAL_UNITS.MILLION) { @@ -59,20 +56,16 @@ export function toShortNumber(sourceNumber) { * pluralReady(1793, DECIMAL_UNITS.THOUSAND) * // => 1790 */ -export function pluralReady(sourceNumber, division) { +export function pluralReady(sourceNumber: number, division: DecimalUnits): number { if (division == null || division < DECIMAL_UNITS.HUNDRED) { return sourceNumber; } - let closestScale = division / DECIMAL_UNITS.TEN; + const closestScale = division / DECIMAL_UNITS.TEN; return Math.trunc(sourceNumber / closestScale) * closestScale; } -/** - * @param {number} num - * @returns {number} - */ -export function roundTo10(num) { +export function roundTo10(num: number): number { return Math.round(num * 0.1) / 0.1; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index bd01d8bd1e..4c36411a33 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1784,7 +1784,6 @@ a.account__display-name { .status__avatar { width: 46px; height: 46px; - box-shadow: 0 0 0 2px $ui-base-color; } .muted { @@ -3110,6 +3109,10 @@ $ui-header-height: 55px; } .compose-form__highlightable { + display: flex; + flex-direction: column; + overflow: hidden; + flex: 0 1 auto; border-radius: 4px; transition: box-shadow 300ms linear; @@ -3804,6 +3807,10 @@ a.status-card { } .status-card-video { + // Firefox has a bug where frameborder=0 iframes add some extra blank space + // see https://bugzilla.mozilla.org/show_bug.cgi?id=155174 + overflow: hidden; + iframe { width: 100%; height: 100%; @@ -6326,30 +6333,25 @@ a.status-card.compact:hover { z-index: 9999; } -.media-gallery__gifv__label { - display: block; +.media-gallery__item__badges { position: absolute; - color: $primary-text-color; - background: rgba($base-overlay-background, 0.5); bottom: 6px; inset-inline-start: 6px; - padding: 2px 6px; - border-radius: 2px; - font-size: 11px; - font-weight: 600; - z-index: 1; - pointer-events: none; - opacity: 0.9; - transition: opacity 0.1s ease; - line-height: 18px; + display: flex; + gap: 2px; } -.media-gallery__gifv { - &:hover { - .media-gallery__gifv__label { - opacity: 1; - } - } +.media-gallery__gifv__label { + display: block; + color: $white; + background: rgba($black, 0.65); + padding: 2px 6px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; + z-index: 1; + pointer-events: none; + line-height: 18px; } .attachment-list { @@ -6424,17 +6426,28 @@ a.status-card.compact:hover { position: relative; width: 100%; min-height: 64px; + display: grid; + grid-template-columns: 50% 50%; + grid-template-rows: 50% 50%; + gap: 2px; } .media-gallery__item { border: 0; box-sizing: border-box; display: block; - float: left; position: relative; border-radius: 4px; overflow: hidden; + &--tall { + grid-row: span 2; + } + + &--wide { + grid-column: span 2; + } + &.standalone { .media-gallery__item-gifv-thumbnail { transform: none; @@ -8332,6 +8345,7 @@ noscript { font-weight: 500; cursor: pointer; color: $darker-text-color; + aspect-ratio: 16 / 9; i { display: block; diff --git a/app/javascript/types/resources.ts b/app/javascript/types/resources.ts index 372ff7523b..28fad2719a 100644 --- a/app/javascript/types/resources.ts +++ b/app/javascript/types/resources.ts @@ -1,8 +1,4 @@ -interface MastodonMap<T> { - get<K extends keyof T>(key: K): T[K]; - has<K extends keyof T>(key: K): boolean; - set<K extends keyof T>(key: K, value: T[K]): this; -} +import type { Record } from 'immutable'; type AccountValues = { id: number; @@ -10,4 +6,5 @@ type AccountValues = { avatar_static: string; [key: string]: any; }; -export type Account = MastodonMap<AccountValues>; + +export type Account = Record<AccountValues>; diff --git a/app/javascript/types/util.ts b/app/javascript/types/util.ts new file mode 100644 index 0000000000..5f2cf2cf07 --- /dev/null +++ b/app/javascript/types/util.ts @@ -0,0 +1 @@ +export type ValueOf<T> = T[keyof T]; diff --git a/app/lib/activity_tracker.rb b/app/lib/activity_tracker.rb index 8829d8605f..9160ef22a6 100644 --- a/app/lib/activity_tracker.rb +++ b/app/lib/activity_tracker.rb @@ -43,7 +43,7 @@ class ActivityTracker case @type when :basic - redis.mget(*keys).map(&:to_i).sum + redis.mget(*keys).sum(&:to_i) when :unique redis.pfcount(*keys) end diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index e6674be8ae..9dcafff3ab 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -4,7 +4,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity def perform return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity? - with_lock("announce:#{value_or_id(@object)}") do + with_redis_lock("announce:#{value_or_id(@object)}") do original_status = status_from_object return reject_payload! if original_status.nil? || !announceable?(original_status) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index b1f7a7798f..3fec5a2f7f 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -47,7 +47,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def create_status return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity? - with_lock("create:#{object_uri}") do + with_redis_lock("create:#{object_uri}") do return if delete_arrived_first?(object_uri) || poll_vote? @status = find_existing_status @@ -313,7 +313,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity poll = replied_to_status.preloadable_poll already_voted = true - with_lock("vote:#{replied_to_status.poll_id}:#{@account.id}") do + with_redis_lock("vote:#{replied_to_status.poll_id}:#{@account.id}") do already_voted = poll.votes.where(account: @account).exists? poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: object_uri) end diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 3af500f2b1..61f6ca6997 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -12,7 +12,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity private def delete_person - with_lock("delete_in_progress:#{@account.id}", autorelease: 2.hours, raise_on_failure: false) do + with_redis_lock("delete_in_progress:#{@account.id}", autorelease: 2.hours, raise_on_failure: false) do DeleteAccountService.new.call(@account, reserve_username: false, skip_activitypub: true) end end @@ -20,14 +20,14 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def delete_note return if object_uri.nil? - with_lock("delete_status_in_progress:#{object_uri}", raise_on_failure: false) do + with_redis_lock("delete_status_in_progress:#{object_uri}", raise_on_failure: false) do unless non_matching_uri_hosts?(@account.uri, object_uri) # This lock ensures a concurrent `ActivityPub::Activity::Create` either # does not create a status at all, or has finished saving it to the # database before we try to load it. # Without the lock, `delete_later!` could be called after `delete_arrived_first?` # and `Status.find` before `Status.create!` - with_lock("create:#{object_uri}") { delete_later!(object_uri) } + with_redis_lock("create:#{object_uri}") { delete_later!(object_uri) } Tombstone.find_or_create_by(uri: object_uri, account: @account) end diff --git a/app/lib/activitypub/case_transform.rb b/app/lib/activitypub/case_transform.rb index d36e01b8f2..da2c5eb8b0 100644 --- a/app/lib/activitypub/case_transform.rb +++ b/app/lib/activitypub/case_transform.rb @@ -13,7 +13,7 @@ module ActivityPub::CaseTransform when Symbol then camel_lower(value.to_s).to_sym when String camel_lower_cache[value] ||= if value.start_with?('_:') - "_:#{value.gsub(/\A_:/, '').underscore.camelize(:lower)}" + "_:#{value.delete_prefix('_:').underscore.camelize(:lower)}" else value.underscore.camelize(:lower) end diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 15ff6d15f1..e98ae2d704 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -407,10 +407,10 @@ class FeedManager return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language) check_for_blocks = crutches[:active_mentions][status.id] || [] - check_for_blocks.concat([status.account_id]) + check_for_blocks.push(status.account_id) if status.reblog? - check_for_blocks.concat([status.reblog.account_id]) + check_for_blocks.push(status.reblog.account_id) check_for_blocks.concat(crutches[:active_mentions][status.reblog_of_id] || []) end @@ -446,7 +446,7 @@ class FeedManager # the notification has been checked for mute/block. Therefore, it's not # necessary to check the author of the toot for mute/block again check_for_blocks = status.active_mentions.pluck(:account_id) - check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil? + check_for_blocks.push(status.in_reply_to_account) if status.reply? && !status.in_reply_to_account_id.nil? should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted) should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them @@ -593,10 +593,10 @@ class FeedManager check_for_blocks = statuses.flat_map do |s| arr = crutches[:active_mentions][s.id] || [] - arr.concat([s.account_id]) + arr.push(s.account_id) if s.reblog? - arr.concat([s.reblog.account_id]) + arr.push(s.reblog.account_id) arr.concat(crutches[:active_mentions][s.reblog_of_id] || []) end diff --git a/app/lib/importer/accounts_index_importer.rb b/app/lib/importer/accounts_index_importer.rb index 792a31b1bd..fd869c3960 100644 --- a/app/lib/importer/accounts_index_importer.rb +++ b/app/lib/importer/accounts_index_importer.rb @@ -6,8 +6,8 @@ class Importer::AccountsIndexImporter < Importer::BaseImporter in_work_unit(tmp) do |accounts| bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: accounts).bulk_body - indexed = bulk.select { |entry| entry[:index] }.size - deleted = bulk.select { |entry| entry[:delete] }.size + indexed = bulk.count { |entry| entry[:index] } + deleted = bulk.count { |entry| entry[:delete] } Chewy::Index::Import::BulkRequest.new(index).perform(bulk) diff --git a/app/lib/importer/tags_index_importer.rb b/app/lib/importer/tags_index_importer.rb index f5bd8f052b..77710ed7de 100644 --- a/app/lib/importer/tags_index_importer.rb +++ b/app/lib/importer/tags_index_importer.rb @@ -6,8 +6,8 @@ class Importer::TagsIndexImporter < Importer::BaseImporter in_work_unit(tmp) do |tags| bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: tags).bulk_body - indexed = bulk.select { |entry| entry[:index] }.size - deleted = bulk.select { |entry| entry[:delete] }.size + indexed = bulk.count { |entry| entry[:index] } + deleted = bulk.count { |entry| entry[:delete] } Chewy::Index::Import::BulkRequest.new(index).perform(bulk) diff --git a/app/lib/permalink_redirector.rb b/app/lib/permalink_redirector.rb index cf1a376251..0fcec683d9 100644 --- a/app/lib/permalink_redirector.rb +++ b/app/lib/permalink_redirector.rb @@ -8,21 +8,51 @@ class PermalinkRedirector end def redirect_path - if path_segments[0].present? && path_segments[0].start_with?('@') && path_segments[1] =~ /\d/ - find_status_url_by_id(path_segments[1]) - elsif path_segments[0].present? && path_segments[0].start_with?('@') - find_account_url_by_name(path_segments[0]) - elsif path_segments[0] == 'statuses' && path_segments[1] =~ /\d/ - find_status_url_by_id(path_segments[1]) - elsif path_segments[0] == 'accounts' && path_segments[1] =~ /\d/ - find_account_url_by_id(path_segments[1]) + if at_username_status_request? || statuses_status_request? + find_status_url_by_id(second_segment) + elsif at_username_request? + find_account_url_by_name(first_segment) + elsif accounts_request? && record_integer_id_request? + find_account_url_by_id(second_segment) end end private + def at_username_status_request? + at_username_request? && record_integer_id_request? + end + + def statuses_status_request? + statuses_request? && record_integer_id_request? + end + + def at_username_request? + first_segment.present? && first_segment.start_with?('@') + end + + def statuses_request? + first_segment == 'statuses' + end + + def accounts_request? + first_segment == 'accounts' + end + + def record_integer_id_request? + second_segment =~ /\d/ + end + + def first_segment + path_segments.first + end + + def second_segment + path_segments.second + end + def path_segments - @path_segments ||= @path.gsub(/\A\//, '').split('/') + @path_segments ||= @path.delete_prefix('/').split('/') end def find_status_url_by_id(id) diff --git a/app/lib/vacuum/imports_vacuum.rb b/app/lib/vacuum/imports_vacuum.rb new file mode 100644 index 0000000000..ffb9449a42 --- /dev/null +++ b/app/lib/vacuum/imports_vacuum.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Vacuum::ImportsVacuum + def perform + clean_unconfirmed_imports! + clean_old_imports! + end + + private + + def clean_unconfirmed_imports! + BulkImport.where(state: :unconfirmed).where('created_at <= ?', 10.minutes.ago).reorder(nil).in_batches.delete_all + end + + def clean_old_imports! + BulkImport.where('created_at <= ?', 1.week.ago).reorder(nil).in_batches.delete_all + end +end diff --git a/app/lib/webfinger_resource.rb b/app/lib/webfinger_resource.rb index 4209454859..7e1a7196d7 100644 --- a/app/lib/webfinger_resource.rb +++ b/app/lib/webfinger_resource.rb @@ -57,7 +57,7 @@ class WebfingerResource end def resource_without_acct_string - resource.gsub(/\Aacct:/, '') + resource.delete_prefix('acct:') end def local_username diff --git a/app/models/account.rb b/app/models/account.rb index 9aefbab900..30af67615b 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -78,6 +78,7 @@ class Account < ApplicationRecord include DomainNormalizable include DomainMaterializable include AccountMerging + include AccountSearch MAX_DISPLAY_NAME_LENGTH = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i MAX_NOTE_LENGTH = (ENV['MAX_BIO_CHARS'] || 500).to_i @@ -408,14 +409,6 @@ class Account < ApplicationRecord end class << self - DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’]/ - TEXTSEARCH = "(setweight(to_tsvector('simple', accounts.display_name), 'A') || setweight(to_tsvector('simple', accounts.username), 'B') || setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C'))" - - REPUTATION_SCORE_FUNCTION = '(greatest(0, coalesce(s.followers_count, 0)) / (greatest(0, coalesce(s.following_count, 0)) + 1.0))' - FOLLOWERS_SCORE_FUNCTION = 'log(greatest(0, coalesce(s.followers_count, 0)) + 2)' - TIME_DISTANCE_FUNCTION = '(case when s.last_status_at is null then 0 else exp(-1.0 * ((greatest(0, abs(extract(DAY FROM age(s.last_status_at))) - 30.0)^2) / (2.0 * ((-1.0 * 30^2) / (2.0 * ln(0.3)))))) end)' - BOOST = "((#{REPUTATION_SCORE_FUNCTION} + #{FOLLOWERS_SCORE_FUNCTION} + #{TIME_DISTANCE_FUNCTION}) / 3.0)" - def readonly_attributes super - %w(statuses_count following_count followers_count) end @@ -425,37 +418,6 @@ class Account < ApplicationRecord DeliveryFailureTracker.without_unavailable(urls) end - def search_for(terms, limit: 10, offset: 0) - tsquery = generate_query_for_search(terms) - - sql = <<-SQL.squish - SELECT - accounts.*, - #{BOOST} * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank - FROM accounts - LEFT JOIN users ON accounts.id = users.account_id - LEFT JOIN account_stats AS s ON accounts.id = s.account_id - WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH} - AND accounts.suspended_at IS NULL - AND accounts.moved_to_account_id IS NULL - AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL)) - ORDER BY rank DESC - LIMIT :limit OFFSET :offset - SQL - - records = find_by_sql([sql, limit: limit, offset: offset, tsquery: tsquery]) - ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) - records - end - - def advanced_search_for(terms, account, limit: 10, following: false, offset: 0) - tsquery = generate_query_for_search(terms) - sql = advanced_search_for_sql_template(following) - records = find_by_sql([sql, id: account.id, limit: limit, offset: offset, tsquery: tsquery]) - ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) - records - end - def from_text(text) return [] if text.blank? @@ -469,73 +431,15 @@ class Account < ApplicationRecord EntityCache.instance.mention(username, domain) end end - - private - - def generate_query_for_search(unsanitized_terms) - terms = unsanitized_terms.gsub(DISALLOWED_TSQUERY_CHARACTERS, ' ') - - # The final ":*" is for prefix search. - # The trailing space does not seem to fit any purpose, but `to_tsquery` - # behaves differently with and without a leading space if the terms start - # with `./`, `../`, or `.. `. I don't understand why, so, in doubt, keep - # the same query. - "' #{terms} ':*" - end - - def advanced_search_for_sql_template(following) - if following - <<-SQL.squish - WITH first_degree AS ( - SELECT target_account_id - FROM follows - WHERE account_id = :id - UNION ALL - SELECT :id - ) - SELECT - accounts.*, - (count(f.id) + 1) * #{BOOST} * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank - FROM accounts - LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id) - LEFT JOIN account_stats AS s ON accounts.id = s.account_id - WHERE accounts.id IN (SELECT * FROM first_degree) - AND to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH} - AND accounts.suspended_at IS NULL - AND accounts.moved_to_account_id IS NULL - GROUP BY accounts.id, s.id - ORDER BY rank DESC - LIMIT :limit OFFSET :offset - SQL - else - <<-SQL.squish - SELECT - accounts.*, - #{BOOST} * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank, - count(f.id) AS followed - FROM accounts - LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id) OR (accounts.id = f.target_account_id AND f.account_id = :id) - LEFT JOIN users ON accounts.id = users.account_id - LEFT JOIN account_stats AS s ON accounts.id = s.account_id - WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH} - AND accounts.suspended_at IS NULL - AND accounts.moved_to_account_id IS NULL - AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL)) - GROUP BY accounts.id, s.id - ORDER BY followed DESC, rank DESC - LIMIT :limit OFFSET :offset - SQL - end - end end def emojis @emojis ||= CustomEmoji.from_text(emojifiable_text, domain) end - before_create :generate_keys before_validation :prepare_contents, if: :local? before_validation :prepare_username, on: :create + before_create :generate_keys before_destroy :clean_feed_manager def ensure_keys! diff --git a/app/models/account_conversation.rb b/app/models/account_conversation.rb index b3ddc04c10..be07879219 100644 --- a/app/models/account_conversation.rb +++ b/app/models/account_conversation.rb @@ -17,14 +17,13 @@ class AccountConversation < ApplicationRecord include Redisable + before_validation :set_last_status after_commit :push_to_streaming_api belongs_to :account belongs_to :conversation belongs_to :last_status, class_name: 'Status' - before_validation :set_last_status - def participant_account_ids=(arr) self[:participant_account_ids] = arr.sort end diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb index fa8cb6013c..b9da596172 100644 --- a/app/models/account_migration.rb +++ b/app/models/account_migration.rb @@ -42,7 +42,7 @@ class AccountMigration < ApplicationRecord return false unless errors.empty? - with_lock("account_migration:#{account.id}") do + with_redis_lock("account_migration:#{account.id}") do save end end diff --git a/app/models/account_statuses_filter.rb b/app/models/account_statuses_filter.rb index 556aee032e..849183aa28 100644 --- a/app/models/account_statuses_filter.rb +++ b/app/models/account_statuses_filter.rb @@ -32,9 +32,9 @@ class AccountStatusesFilter private def initial_scope - if suspended? - Status.none - elsif anonymous? + return Status.none if suspended? + + if anonymous? account.statuses.not_local_only.where(visibility: %i(public unlisted)) elsif author? account.statuses.all # NOTE: #merge! does not work without the #all diff --git a/app/models/account_suggestions/source.rb b/app/models/account_suggestions/source.rb index bd1068d201..be462cd0f5 100644 --- a/app/models/account_suggestions/source.rb +++ b/app/models/account_suggestions/source.rb @@ -18,7 +18,7 @@ class AccountSuggestions::Source def as_ordered_suggestions(scope, ordered_list) return [] if ordered_list.empty? - map = scope.index_by(&method(:to_ordered_list_key)) + map = scope.index_by { |account| to_ordered_list_key(account) } ordered_list.map { |ordered_list_key| map[ordered_list_key] }.compact.map do |account| AccountSuggestions::Suggestion.new( diff --git a/app/models/admin/appeal_filter.rb b/app/models/admin/appeal_filter.rb index f5dcc0f54d..24f8df059c 100644 --- a/app/models/admin/appeal_filter.rb +++ b/app/models/admin/appeal_filter.rb @@ -5,6 +5,8 @@ class Admin::AppealFilter status ).freeze + IGNORED_PARAMS = %w(page).freeze + attr_reader :params def initialize(params) @@ -15,7 +17,7 @@ class Admin::AppealFilter scope = Appeal.order(id: :desc) params.each do |key, value| - next if %w(page).include?(key.to_s) + next if IGNORED_PARAMS.include?(key.to_s) scope.merge!(scope_for(key, value.to_s.strip)) if value.present? end diff --git a/app/models/admin/status_filter.rb b/app/models/admin/status_filter.rb index 4d439e9a1c..645c2e6207 100644 --- a/app/models/admin/status_filter.rb +++ b/app/models/admin/status_filter.rb @@ -6,6 +6,8 @@ class Admin::StatusFilter report_id ).freeze + IGNORED_PARAMS = %w(page report_id).freeze + attr_reader :params def initialize(account, params) @@ -17,7 +19,7 @@ class Admin::StatusFilter scope = @account.statuses.where(visibility: [:public, :unlisted]) params.each do |key, value| - next if %w(page report_id).include?(key.to_s) + next if IGNORED_PARAMS.include?(key.to_s) scope.merge!(scope_for(key, value.to_s.strip)) if value.present? end diff --git a/app/models/announcement_reaction.rb b/app/models/announcement_reaction.rb index d22771034f..9881892c4b 100644 --- a/app/models/announcement_reaction.rb +++ b/app/models/announcement_reaction.rb @@ -14,6 +14,7 @@ # class AnnouncementReaction < ApplicationRecord + before_validation :set_custom_emoji after_commit :queue_publish belongs_to :account @@ -23,8 +24,6 @@ class AnnouncementReaction < ApplicationRecord validates :name, presence: true validates_with ReactionValidator - before_validation :set_custom_emoji - private def set_custom_emoji diff --git a/app/models/block.rb b/app/models/block.rb index b42c1569b9..11156ebab3 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -25,8 +25,8 @@ class Block < ApplicationRecord false # Force uri_for to use uri attribute end - after_commit :remove_blocking_cache before_validation :set_uri, only: :create + after_commit :remove_blocking_cache private diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb new file mode 100644 index 0000000000..af9a9670bf --- /dev/null +++ b/app/models/bulk_import.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: bulk_imports +# +# id :bigint(8) not null, primary key +# type :integer not null +# state :integer not null +# total_items :integer default(0), not null +# imported_items :integer default(0), not null +# processed_items :integer default(0), not null +# finished_at :datetime +# overwrite :boolean default(FALSE), not null +# likely_mismatched :boolean default(FALSE), not null +# original_filename :string default(""), not null +# account_id :bigint(8) not null +# created_at :datetime not null +# updated_at :datetime not null +# +class BulkImport < ApplicationRecord + self.inheritance_column = false + + belongs_to :account + has_many :rows, class_name: 'BulkImportRow', inverse_of: :bulk_import, dependent: :delete_all + + enum type: { + following: 0, + blocking: 1, + muting: 2, + domain_blocking: 3, + bookmarks: 4, + } + + enum state: { + unconfirmed: 0, + scheduled: 1, + in_progress: 2, + finished: 3, + } + + validates :type, presence: true + + def self.progress!(bulk_import_id, imported: false) + # Use `increment_counter` so that the incrementation is done atomically in the database + BulkImport.increment_counter(:processed_items, bulk_import_id) # rubocop:disable Rails/SkipsModelValidations + BulkImport.increment_counter(:imported_items, bulk_import_id) if imported # rubocop:disable Rails/SkipsModelValidations + + # Since the incrementation has been done atomically, concurrent access to `bulk_import` is now bening + bulk_import = BulkImport.find(bulk_import_id) + bulk_import.update!(state: :finished, finished_at: Time.now.utc) if bulk_import.processed_items == bulk_import.total_items + end +end diff --git a/app/models/bulk_import_row.rb b/app/models/bulk_import_row.rb new file mode 100644 index 0000000000..dd7190c970 --- /dev/null +++ b/app/models/bulk_import_row.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: bulk_import_rows +# +# id :bigint(8) not null, primary key +# bulk_import_id :bigint(8) not null +# data :jsonb +# created_at :datetime not null +# updated_at :datetime not null +# +class BulkImportRow < ApplicationRecord + belongs_to :bulk_import +end diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index bbe269e8f0..592812e960 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -68,5 +68,8 @@ module AccountAssociations # Account statuses cleanup policy has_one :statuses_cleanup_policy, class_name: 'AccountStatusesCleanupPolicy', inverse_of: :account, dependent: :destroy + + # Imports + has_many :bulk_imports, inverse_of: :account, dependent: :delete_all end end diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index b2ccddef32..3c64ebd9fa 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -271,7 +271,8 @@ module AccountInteractions end def lists_for_local_distribution - lists.joins(account: :user) + scope = lists.joins(account: :user) + scope.where.not(list_accounts: { follow_id: nil }).or(scope.where(account_id: id)) .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago) end diff --git a/app/models/concerns/account_search.rb b/app/models/concerns/account_search.rb new file mode 100644 index 0000000000..67d77793fe --- /dev/null +++ b/app/models/concerns/account_search.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +module AccountSearch + extend ActiveSupport::Concern + + DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’]/ + + TEXT_SEARCH_RANKS = <<~SQL.squish + ( + setweight(to_tsvector('simple', accounts.display_name), 'A') || + setweight(to_tsvector('simple', accounts.username), 'B') || + setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C') + ) + SQL + + REPUTATION_SCORE_FUNCTION = <<~SQL.squish + ( + greatest(0, coalesce(s.followers_count, 0)) / ( + greatest(0, coalesce(s.following_count, 0)) + 1.0 + ) + ) + SQL + + FOLLOWERS_SCORE_FUNCTION = <<~SQL.squish + log( + greatest(0, coalesce(s.followers_count, 0)) + 2 + ) + SQL + + TIME_DISTANCE_FUNCTION = <<~SQL.squish + ( + case + when s.last_status_at is null then 0 + else exp( + -1.0 * ( + ( + greatest(0, abs(extract(DAY FROM age(s.last_status_at))) - 30.0)^2) /#{' '} + (2.0 * ((-1.0 * 30^2) / (2.0 * ln(0.3))) + ) + ) + ) + end + ) + SQL + + BOOST = <<~SQL.squish + ( + (#{REPUTATION_SCORE_FUNCTION} + #{FOLLOWERS_SCORE_FUNCTION} + #{TIME_DISTANCE_FUNCTION}) / 3.0 + ) + SQL + + BASIC_SEARCH_SQL = <<~SQL.squish + SELECT + accounts.*, + #{BOOST} * ts_rank_cd(#{TEXT_SEARCH_RANKS}, to_tsquery('simple', :tsquery), 32) AS rank + FROM accounts + LEFT JOIN users ON accounts.id = users.account_id + LEFT JOIN account_stats AS s ON accounts.id = s.account_id + WHERE to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS} + AND accounts.suspended_at IS NULL + AND accounts.moved_to_account_id IS NULL + AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL)) + ORDER BY rank DESC + LIMIT :limit OFFSET :offset + SQL + + ADVANCED_SEARCH_WITH_FOLLOWING = <<~SQL.squish + WITH first_degree AS ( + SELECT target_account_id + FROM follows + WHERE account_id = :id + UNION ALL + SELECT :id + ) + SELECT + accounts.*, + (count(f.id) + 1) * #{BOOST} * ts_rank_cd(#{TEXT_SEARCH_RANKS}, to_tsquery('simple', :tsquery), 32) AS rank + FROM accounts + LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id) + LEFT JOIN account_stats AS s ON accounts.id = s.account_id + WHERE accounts.id IN (SELECT * FROM first_degree) + AND to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS} + AND accounts.suspended_at IS NULL + AND accounts.moved_to_account_id IS NULL + GROUP BY accounts.id, s.id + ORDER BY rank DESC + LIMIT :limit OFFSET :offset + SQL + + ADVANCED_SEARCH_WITHOUT_FOLLOWING = <<~SQL.squish + SELECT + accounts.*, + #{BOOST} * ts_rank_cd(#{TEXT_SEARCH_RANKS}, to_tsquery('simple', :tsquery), 32) AS rank, + count(f.id) AS followed + FROM accounts + LEFT OUTER JOIN follows AS f ON + (accounts.id = f.account_id AND f.target_account_id = :id) OR (accounts.id = f.target_account_id AND f.account_id = :id) + LEFT JOIN users ON accounts.id = users.account_id + LEFT JOIN account_stats AS s ON accounts.id = s.account_id + WHERE to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS} + AND accounts.suspended_at IS NULL + AND accounts.moved_to_account_id IS NULL + AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL)) + GROUP BY accounts.id, s.id + ORDER BY followed DESC, rank DESC + LIMIT :limit OFFSET :offset + SQL + + class_methods do + def search_for(terms, limit: 10, offset: 0) + tsquery = generate_query_for_search(terms) + + find_by_sql([BASIC_SEARCH_SQL, { limit: limit, offset: offset, tsquery: tsquery }]).tap do |records| + ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) + end + end + + def advanced_search_for(terms, account, limit: 10, following: false, offset: 0) + tsquery = generate_query_for_search(terms) + sql_template = following ? ADVANCED_SEARCH_WITH_FOLLOWING : ADVANCED_SEARCH_WITHOUT_FOLLOWING + + find_by_sql([sql_template, { id: account.id, limit: limit, offset: offset, tsquery: tsquery }]).tap do |records| + ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) + end + end + + private + + def generate_query_for_search(unsanitized_terms) + terms = unsanitized_terms.gsub(DISALLOWED_TSQUERY_CHARACTERS, ' ') + + # The final ":*" is for prefix search. + # The trailing space does not seem to fit any purpose, but `to_tsquery` + # behaves differently with and without a leading space if the terms start + # with `./`, `../`, or `.. `. I don't understand why, so, in doubt, keep + # the same query. + "' #{terms} ':*" + end + end +end diff --git a/app/models/concerns/lockable.rb b/app/models/concerns/lockable.rb index 55a9714ca8..3354ce1a99 100644 --- a/app/models/concerns/lockable.rb +++ b/app/models/concerns/lockable.rb @@ -5,7 +5,7 @@ module Lockable # @param [ActiveSupport::Duration] autorelease Automatically release the lock after this time # @param [Boolean] raise_on_failure Raise an error if a lock cannot be acquired, or fail silently # @raise [Mastodon::RaceConditionError] - def with_lock(lock_name, autorelease: 15.minutes, raise_on_failure: true) + def with_redis_lock(lock_name, autorelease: 15.minutes, raise_on_failure: true) with_redis do |redis| RedisLock.acquire(redis: redis, key: "lock:#{lock_name}", autorelease: autorelease.seconds) do |lock| if lock.acquired? diff --git a/app/models/concerns/status_safe_reblog_insert.rb b/app/models/concerns/status_safe_reblog_insert.rb new file mode 100644 index 0000000000..a7ccb52e9a --- /dev/null +++ b/app/models/concerns/status_safe_reblog_insert.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module StatusSafeReblogInsert + extend ActiveSupport::Concern + + class_methods do + # This is a hack to ensure that no reblogs of discarded statuses are created, + # as this cannot be enforced through database constraints the same way we do + # for reblogs of deleted statuses. + # + # To achieve this, we redefine the internal method responsible for issuing + # the "INSERT" statement and replace the "INSERT INTO ... VALUES ..." query + # with an "INSERT INTO ... SELECT ..." query with a "WHERE deleted_at IS NULL" + # clause on the reblogged status to ensure consistency at the database level. + # + # Otherwise, the code is kept as close as possible to ActiveRecord::Persistence + # code, and actually calls it if we are not handling a reblog. + def _insert_record(values) + return super unless values.is_a?(Hash) && values['reblog_of_id'].present? + + primary_key = self.primary_key + primary_key_value = nil + + if primary_key + primary_key_value = values[primary_key] + + if !primary_key_value && prefetch_primary_key? + primary_key_value = next_sequence_value + values[primary_key] = primary_key_value + end + end + + # The following line is where we differ from stock ActiveRecord implementation + im = _compile_reblog_insert(values) + + # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible. + # For our purposes, it's equivalent to a foreign key constraint violation + result = connection.insert(im, "#{self} Create", primary_key || false, primary_key_value) + raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id']}) is not present in table \"statuses\"" if result.nil? + + result + end + + def _compile_reblog_insert(values) + # This is somewhat equivalent to the following code of ActiveRecord::Persistence: + # `arel_table.compile_insert(_substitute_values(values))` + # The main difference is that we use a `SELECT` instead of a `VALUES` clause, + # which means we have to build the `SELECT` clause ourselves and do a bit more + # manual work. + + # Instead of using Arel::InsertManager#values, we are going to use Arel::InsertManager#select + im = Arel::InsertManager.new + im.into(arel_table) + + binds = [] + reblog_bind = nil + values.each do |name, value| + attr = arel_table[name] + bind = predicate_builder.build_bind_attribute(attr.name, value) + + im.columns << attr + binds << bind + + reblog_bind = bind if name == 'reblog_of_id' + end + + im.select(arel_table.where(arel_table[:id].eq(reblog_bind)).where(arel_table[:deleted_at].eq(nil)).project(*binds)) + + im + end + end +end diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 78f79c18f0..a5c23e09d4 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -32,7 +32,8 @@ class FollowRequest < ApplicationRecord validates :languages, language: true def authorize! - account.follow!(target_account, reblogs: show_reblogs, notify: notify, languages: languages, uri: uri, bypass_limit: true) + follow = account.follow!(target_account, reblogs: show_reblogs, notify: notify, languages: languages, uri: uri, bypass_limit: true) + ListAccount.where(follow_request: self).update_all(follow_request_id: nil, follow_id: follow.id) # rubocop:disable Rails/SkipsModelValidations MergeWorker.perform_async(target_account.id, account.id) if account.local? destroy! end diff --git a/app/models/form/import.rb b/app/models/form/import.rb new file mode 100644 index 0000000000..750ef84be4 --- /dev/null +++ b/app/models/form/import.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require 'csv' + +# A non-ActiveRecord helper class for CSV uploads. +# Handles saving contents to database. +class Form::Import + include ActiveModel::Model + + MODES = %i(merge overwrite).freeze + + FILE_SIZE_LIMIT = 20.megabytes + ROWS_PROCESSING_LIMIT = 20_000 + + EXPECTED_HEADERS_BY_TYPE = { + following: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'], + blocking: ['Account address'], + muting: ['Account address', 'Hide notifications'], + domain_blocking: ['#domain'], + bookmarks: ['#uri'], + }.freeze + + KNOWN_FIRST_HEADERS = EXPECTED_HEADERS_BY_TYPE.values.map(&:first).uniq.freeze + + ATTRIBUTE_BY_HEADER = { + 'Account address' => 'acct', + 'Show boosts' => 'show_reblogs', + 'Notify on new posts' => 'notify', + 'Languages' => 'languages', + 'Hide notifications' => 'hide_notifications', + '#domain' => 'domain', + '#uri' => 'uri', + }.freeze + + class EmptyFileError < StandardError; end + + attr_accessor :current_account, :data, :type, :overwrite, :bulk_import + + validates :type, presence: true + validates :data, presence: true + validate :validate_data + + def guessed_type + return :muting if csv_data.headers.include?('Hide notifications') + return :following if csv_data.headers.include?('Show boosts') || csv_data.headers.include?('Notify on new posts') || csv_data.headers.include?('Languages') + return :following if data.original_filename&.start_with?('follows') || data.original_filename&.start_with?('following_accounts') + return :blocking if data.original_filename&.start_with?('blocks') || data.original_filename&.start_with?('blocked_accounts') + return :muting if data.original_filename&.start_with?('mutes') || data.original_filename&.start_with?('muted_accounts') + return :domain_blocking if data.original_filename&.start_with?('domain_blocks') || data.original_filename&.start_with?('blocked_domains') + return :bookmarks if data.original_filename&.start_with?('bookmarks') + end + + # Whether the uploaded CSV file seems to correspond to a different import type than the one selected + def likely_mismatched? + guessed_type.present? && guessed_type != type.to_sym + end + + def save + return false unless valid? + + ApplicationRecord.transaction do + now = Time.now.utc + @bulk_import = current_account.bulk_imports.create(type: type, overwrite: overwrite || false, state: :unconfirmed, original_filename: data.original_filename, likely_mismatched: likely_mismatched?) + nb_items = BulkImportRow.insert_all(parsed_rows.map { |row| { bulk_import_id: bulk_import.id, data: row, created_at: now, updated_at: now } }).length # rubocop:disable Rails/SkipsModelValidations + @bulk_import.update(total_items: nb_items) + end + end + + def mode + overwrite ? :overwrite : :merge + end + + def mode=(str) + self.overwrite = str.to_sym == :overwrite + end + + private + + def default_csv_header + case type.to_sym + when :following, :blocking, :muting + 'Account address' + when :domain_blocking + '#domain' + when :bookmarks + '#uri' + end + end + + def csv_data + return @csv_data if defined?(@csv_data) + + csv_converter = lambda do |field, field_info| + case field_info.header + when 'Show boosts', 'Notify on new posts', 'Hide notifications' + ActiveModel::Type::Boolean.new.cast(field) + when 'Languages' + field&.split(',')&.map(&:strip)&.presence + when 'Account address' + field.strip.gsub(/\A@/, '') + when '#domain', '#uri' + field.strip + else + field + end + end + + @csv_data = CSV.open(data.path, encoding: 'UTF-8', skip_blanks: true, headers: true, converters: csv_converter) + @csv_data.take(1) # Ensure the headers are read + raise EmptyFileError if @csv_data.headers == true + + @csv_data = CSV.open(data.path, encoding: 'UTF-8', skip_blanks: true, headers: [default_csv_header], converters: csv_converter) unless KNOWN_FIRST_HEADERS.include?(@csv_data.headers&.first) + @csv_data + end + + def csv_row_count + return @csv_row_count if defined?(@csv_row_count) + + csv_data.rewind + @csv_row_count = csv_data.take(ROWS_PROCESSING_LIMIT + 2).count + end + + def parsed_rows + csv_data.rewind + + expected_headers = EXPECTED_HEADERS_BY_TYPE[type.to_sym] + + csv_data.take(ROWS_PROCESSING_LIMIT + 1).map do |row| + row.to_h.slice(*expected_headers).transform_keys { |key| ATTRIBUTE_BY_HEADER[key] } + end + end + + def validate_data + return if data.nil? + return errors.add(:data, I18n.t('imports.errors.too_large')) if data.size > FILE_SIZE_LIMIT + return errors.add(:data, I18n.t('imports.errors.incompatible_type')) unless csv_data.headers.include?(default_csv_header) + + errors.add(:data, I18n.t('imports.errors.over_rows_processing_limit', count: ROWS_PROCESSING_LIMIT)) if csv_row_count > ROWS_PROCESSING_LIMIT + + if type.to_sym == :following + base_limit = FollowLimitValidator.limit_for_account(current_account) + limit = base_limit + limit -= current_account.following_count unless overwrite + errors.add(:data, I18n.t('users.follow_limit_reached', limit: base_limit)) if csv_row_count > limit + end + rescue CSV::MalformedCSVError => e + errors.add(:data, I18n.t('imports.errors.invalid_csv_file', error: e.message)) + rescue EmptyFileError + errors.add(:data, I18n.t('imports.errors.empty')) + end +end diff --git a/app/models/import.rb b/app/models/import.rb index 21634005ed..7cd6cccf7c 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -17,6 +17,9 @@ # overwrite :boolean default(FALSE), not null # +# NOTE: This is a deprecated model, only kept to not break ongoing imports +# on upgrade. See `BulkImport` and `Form::Import` for its replacements. + class Import < ApplicationRecord FILE_TYPES = %w(text/plain text/csv application/csv).freeze MODES = %i(merge overwrite).freeze @@ -28,7 +31,6 @@ class Import < ApplicationRecord enum type: { following: 0, blocking: 1, muting: 2, domain_blocking: 3, bookmarks: 4 } validates :type, presence: true - validates_with ImportValidator, on: :create has_attached_file :data validates_attachment_content_type :data, content_type: FILE_TYPES diff --git a/app/models/list_account.rb b/app/models/list_account.rb index a5767d3d8b..e7016f2714 100644 --- a/app/models/list_account.rb +++ b/app/models/list_account.rb @@ -4,24 +4,39 @@ # # Table name: list_accounts # -# id :bigint(8) not null, primary key -# list_id :bigint(8) not null -# account_id :bigint(8) not null -# follow_id :bigint(8) +# id :bigint(8) not null, primary key +# list_id :bigint(8) not null +# account_id :bigint(8) not null +# follow_id :bigint(8) +# follow_request_id :bigint(8) # class ListAccount < ApplicationRecord belongs_to :list belongs_to :account belongs_to :follow, optional: true + belongs_to :follow_request, optional: true validates :account_id, uniqueness: { scope: :list_id } + validate :validate_relationship before_validation :set_follow private def set_follow - self.follow = Follow.find_by!(account_id: list.account_id, target_account_id: account.id) unless list.account_id == account.id + return if list.account_id == account.id + + self.follow = Follow.find_by!(account_id: list.account_id, target_account_id: account.id) + rescue ActiveRecord::RecordNotFound + self.follow_request = FollowRequest.find_by!(account_id: list.account_id, target_account_id: account.id) + end + + def validate_relationship + return if list.account_id == account_id + + errors.add(:account_id, 'follow relationship missing') if follow_id.nil? && follow_request_id.nil? + errors.add(:follow, 'mismatched accounts') if follow_id.present? && follow.target_account_id != account_id + errors.add(:follow_request, 'mismatched accounts') if follow_request_id.present? && follow_request.target_account_id != account_id end end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 0367b4af7b..1d5f3c2880 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -34,8 +34,8 @@ class MediaAttachment < ApplicationRecord include Attachmentable - enum type: { :image => 0, :gifv => 1, :video => 2, :unknown => 3, :audio => 4 } - enum processing: { :queued => 0, :in_progress => 1, :complete => 2, :failed => 3 }, _prefix: true + enum type: { image: 0, gifv: 1, video: 2, unknown: 3, audio: 4 } + enum processing: { queued: 0, in_progress: 1, complete: 2, failed: 3 }, _prefix: true MAX_DESCRIPTION_LENGTH = 1_500 @@ -135,7 +135,7 @@ class MediaAttachment < ApplicationRecord convert_options: { output: { 'loglevel' => 'fatal', - vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', + :vf => 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', }.freeze, }.freeze, format: 'png', @@ -169,6 +169,8 @@ class MediaAttachment < ApplicationRecord original: IMAGE_STYLES[:small].freeze, }.freeze + DEFAULT_STYLES = [:original].freeze + GLOBAL_CONVERT_OPTIONS = { all: '-quality 90 +profile "!icc,*" +set modify-date +set create-date', }.freeze @@ -271,12 +273,12 @@ class MediaAttachment < ApplicationRecord delay_processing? && attachment_name == :file end - after_commit :enqueue_processing, on: :create - after_commit :reset_parent_cache, on: :update - before_create :set_unknown_type before_create :set_processing + after_commit :enqueue_processing, on: :create + after_commit :reset_parent_cache, on: :update + after_post_process :set_meta class << self diff --git a/app/models/relationship_filter.rb b/app/models/relationship_filter.rb index 249fe3df8e..3d75dce05e 100644 --- a/app/models/relationship_filter.rb +++ b/app/models/relationship_filter.rb @@ -10,6 +10,8 @@ class RelationshipFilter location ).freeze + IGNORED_PARAMS = %w(relationship page).freeze + attr_reader :params, :account def initialize(account, params) @@ -23,7 +25,7 @@ class RelationshipFilter scope = scope_for('relationship', params['relationship'].to_s.strip) params.each do |key, value| - next if %w(relationship page).include?(key) + next if IGNORED_PARAMS.include?(key) scope.merge!(scope_for(key.to_s, value.to_s.strip)) if value.present? end diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb index 10c3a6c250..7f5f0d9a9a 100644 --- a/app/models/session_activation.rb +++ b/app/models/session_activation.rb @@ -36,8 +36,8 @@ class SessionActivation < ApplicationRecord detection.platform.id end - before_create :assign_access_token before_save :assign_user_agent + before_create :assign_access_token class << self def active?(id) diff --git a/app/models/status.rb b/app/models/status.rb index b69d2b0649..a52098b602 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -32,14 +32,13 @@ # class Status < ApplicationRecord - before_destroy :unlink_from_conversations! - include Discard::Model include Paginable include Cacheable include StatusThreadingConcern include StatusSnapshotConcern include RateLimitable + include StatusSafeReblogInsert rate_limit by: :account, family: :statuses @@ -119,6 +118,28 @@ class Status < ApplicationRecord after_create_commit :trigger_create_webhooks after_update_commit :trigger_update_webhooks + after_create_commit :increment_counter_caches + after_destroy_commit :decrement_counter_caches + + after_create_commit :store_uri, if: :local? + after_create_commit :update_statistics, if: :local? + + before_validation :prepare_contents, if: :local? + before_validation :set_reblog + before_validation :set_visibility + before_validation :set_conversation + before_validation :set_local + + before_create :set_local_only + + around_create Mastodon::Snowflake::Callbacks + + after_create :set_poll_id + + # The `prepend: true` option below ensures this runs before + # the `dependent: destroy` callbacks remove relevant records + before_destroy :unlink_from_conversations!, prepend: true + cache_associated :application, :media_attachments, :conversation, @@ -316,23 +337,6 @@ class Status < ApplicationRecord attributes['trendable'].nil? && account.requires_review_notification? end - after_create_commit :increment_counter_caches - after_destroy_commit :decrement_counter_caches - - after_create_commit :store_uri, if: :local? - after_create_commit :update_statistics, if: :local? - - before_validation :prepare_contents, if: :local? - before_validation :set_reblog - before_validation :set_visibility - before_validation :set_conversation - before_validation :set_local - before_create :set_locality - - around_create Mastodon::Snowflake::Callbacks - - after_create :set_poll_id - class << self def selectable_visibilities visibilities.keys - %w(direct limited) @@ -442,71 +446,6 @@ class Status < ApplicationRecord super || build_status_stat end - # This is a hack to ensure that no reblogs of discarded statuses are created, - # as this cannot be enforced through database constraints the same way we do - # for reblogs of deleted statuses. - # - # To achieve this, we redefine the internal method responsible for issuing - # the "INSERT" statement and replace the "INSERT INTO ... VALUES ..." query - # with an "INSERT INTO ... SELECT ..." query with a "WHERE deleted_at IS NULL" - # clause on the reblogged status to ensure consistency at the database level. - # - # Otherwise, the code is kept as close as possible to ActiveRecord::Persistence - # code, and actually calls it if we are not handling a reblog. - def self._insert_record(values) - return super unless values.is_a?(Hash) && values['reblog_of_id'].present? - - primary_key = self.primary_key - primary_key_value = nil - - if primary_key - primary_key_value = values[primary_key] - - if !primary_key_value && prefetch_primary_key? - primary_key_value = next_sequence_value - values[primary_key] = primary_key_value - end - end - - # The following line is where we differ from stock ActiveRecord implementation - im = _compile_reblog_insert(values) - - # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible. - # For our purposes, it's equivalent to a foreign key constraint violation - result = connection.insert(im, "#{self} Create", primary_key || false, primary_key_value) - raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id']}) is not present in table \"statuses\"" if result.nil? - - result - end - - def self._compile_reblog_insert(values) - # This is somewhat equivalent to the following code of ActiveRecord::Persistence: - # `arel_table.compile_insert(_substitute_values(values))` - # The main difference is that we use a `SELECT` instead of a `VALUES` clause, - # which means we have to build the `SELECT` clause ourselves and do a bit more - # manual work. - - # Instead of using Arel::InsertManager#values, we are going to use Arel::InsertManager#select - im = Arel::InsertManager.new - im.into(arel_table) - - binds = [] - reblog_bind = nil - values.each do |name, value| - attr = arel_table[name] - bind = predicate_builder.build_bind_attribute(attr.name, value) - - im.columns << attr - binds << bind - - reblog_bind = bind if name == 'reblog_of_id' - end - - im.select(arel_table.where(arel_table[:id].eq(reblog_bind)).where(arel_table[:deleted_at].eq(nil)).project(*binds)) - - im - end - def discard_with_reblogs discard_time = Time.current Status.unscoped.where(reblog_of_id: id, deleted_at: [nil, deleted_at]).in_batches.update_all(deleted_at: discard_time) unless reblog? @@ -555,7 +494,7 @@ class Status < ApplicationRecord self.sensitive = false if sensitive.nil? end - def set_locality + def set_local_only return unless account.domain.nil? && !attribute_changed?(:local_only) self.local_only = marked_local_only? diff --git a/app/models/trends/history.rb b/app/models/trends/history.rb index 83532e7bc8..db36899337 100644 --- a/app/models/trends/history.rb +++ b/app/models/trends/history.rb @@ -11,7 +11,7 @@ class Trends::History end def uses - with_redis { |redis| redis.mget(*@days.map { |day| day.key_for(:uses) }).map(&:to_i).sum } + with_redis { |redis| redis.mget(*@days.map { |day| day.key_for(:uses) }).sum(&:to_i) } end def accounts diff --git a/app/models/trends/preview_card_filter.rb b/app/models/trends/preview_card_filter.rb index f0214c3f0f..ef36ba9878 100644 --- a/app/models/trends/preview_card_filter.rb +++ b/app/models/trends/preview_card_filter.rb @@ -6,6 +6,8 @@ class Trends::PreviewCardFilter locale ).freeze + IGNORED_PARAMS = %w(page).freeze + attr_reader :params def initialize(params) @@ -16,7 +18,7 @@ class Trends::PreviewCardFilter scope = initial_scope params.each do |key, value| - next if %w(page).include?(key.to_s) + next if IGNORED_PARAMS.include?(key.to_s) scope.merge!(scope_for(key, value.to_s.strip)) if value.present? end diff --git a/app/models/trends/status_filter.rb b/app/models/trends/status_filter.rb index de435a0266..da240251fd 100644 --- a/app/models/trends/status_filter.rb +++ b/app/models/trends/status_filter.rb @@ -6,6 +6,8 @@ class Trends::StatusFilter locale ).freeze + IGNORED_PARAMS = %w(page).freeze + attr_reader :params def initialize(params) @@ -16,7 +18,7 @@ class Trends::StatusFilter scope = initial_scope params.each do |key, value| - next if %w(page).include?(key.to_s) + next if IGNORED_PARAMS.include?(key.to_s) scope.merge!(scope_for(key, value.to_s.strip)) if value.present? end diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb index be818a2de7..50d1fb31bd 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class StatusRelationshipsPresenter + PINNABLE_VISIBILITIES = %w(public unlisted private).freeze + attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map, :bookmarks_map, :filters_map @@ -16,7 +18,7 @@ class StatusRelationshipsPresenter statuses = statuses.compact status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact conversation_ids = statuses.filter_map(&:conversation_id).uniq - pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && %w(public unlisted private).include?(s.visibility) } + pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && PINNABLE_VISIBILITIES.include?(s.visibility) } @filters_map = build_filters_map(statuses, current_account_id).merge(options[:filters_map] || {}) @reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {}) diff --git a/app/services/activitypub/fetch_remote_actor_service.rb b/app/services/activitypub/fetch_remote_actor_service.rb index c295700860..8df8c75876 100644 --- a/app/services/activitypub/fetch_remote_actor_service.rb +++ b/app/services/activitypub/fetch_remote_actor_service.rb @@ -67,7 +67,7 @@ class ActivityPub::FetchRemoteActorService < BaseService end def split_acct(acct) - acct.gsub(/\Aacct:/, '').split('@') + acct.delete_prefix('acct:').split('@') end def supported_context? diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index ab0acf7f0f..a491b32b26 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -2,12 +2,15 @@ class ActivityPub::FetchRemoteStatusService < BaseService include JsonLdHelper + include DomainControlHelper include Redisable DISCOVERIES_PER_REQUEST = 1000 # Should be called when uri has already been checked for locality def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil) + return if domain_not_allowed?(uri) + @request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}" @json = if prefetched_body.nil? fetch_resource(uri, id, on_behalf_of) diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 603e4cf48b..ca0083b167 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -24,7 +24,7 @@ class ActivityPub::ProcessAccountService < BaseService # The key does not need to be unguessable, it just needs to be somewhat unique @options[:request_id] ||= "#{Time.now.utc.to_i}-#{username}@#{domain}" - with_lock("process_account:#{@uri}") do + with_redis_lock("process_account:#{@uri}") do @account = Account.remote.find_by(uri: @uri) if @options[:only_key] @account ||= Account.find_remote(@username, @domain) @old_public_key = @account&.public_key diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index ac7372f745..38f6bf2514 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -35,7 +35,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService last_edit_date = @status.edited_at.presence || @status.created_at # Only allow processing one create/update per status at a time - with_lock("create:#{@uri}") do + with_redis_lock("create:#{@uri}") do Status.transaction do record_previous_edit! update_media_attachments! @@ -58,7 +58,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService end def handle_implicit_update! - with_lock("create:#{@uri}") do + with_redis_lock("create:#{@uri}") do update_poll!(allow_significant_changes: false) queue_poll_notifications! end diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index c5e7a8e580..89b8d06cd1 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -1,59 +1,67 @@ # frozen_string_literal: true -require 'rubygems/package' +require 'zip' class BackupService < BaseService include Payloadable + include ContextHelper - attr_reader :account, :backup, :collection + attr_reader :account, :backup def call(backup) @backup = backup @account = backup.user.account - build_json! build_archive! end private - def build_json! - @collection = serialize(collection_presenter, ActivityPub::CollectionSerializer) + def build_outbox_json!(file) + skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer) + skeleton[:@context] = full_context + skeleton[:orderedItems] = ['!PLACEHOLDER!'] + skeleton = Oj.dump(skeleton) + prepend, append = skeleton.split('"!PLACEHOLDER!"') + add_comma = false + + file.write(prepend) account.statuses.with_includes.reorder(nil).find_in_batches do |statuses| - statuses.each do |status| - item = serialize_payload(ActivityPub::ActivityPresenter.from_status(status), ActivityPub::ActivitySerializer, signer: @account, allow_local_only: true) - item.delete(:@context) + file.write(',') if add_comma + add_comma = true + + file.write(statuses.map do |status| + item = serialize_payload(ActivityPub::ActivityPresenter.from_status(status), ActivityPub::ActivitySerializer, allow_local_only: true) + item.delete('@context') unless item[:type] == 'Announce' || item[:object][:attachment].blank? item[:object][:attachment].each do |attachment| - attachment[:url] = Addressable::URI.parse(attachment[:url]).path.gsub(/\A\/system\//, '') + attachment[:url] = Addressable::URI.parse(attachment[:url]).path.delete_prefix('/system/') end end - @collection[:orderedItems] << item - end + Oj.dump(item) + end.join(',')) GC.start end + + file.write(append) end def build_archive! - tmp_file = Tempfile.new(%w(archive .tar.gz)) + tmp_file = Tempfile.new(%w(archive .zip)) - File.open(tmp_file, 'wb') do |file| - Zlib::GzipWriter.wrap(file) do |gz| - Gem::Package::TarWriter.new(gz) do |tar| - dump_media_attachments!(tar) - dump_outbox!(tar) - dump_likes!(tar) - dump_bookmarks!(tar) - dump_actor!(tar) - end - end + Zip::File.open(tmp_file, create: true) do |zipfile| + dump_outbox!(zipfile) + dump_media_attachments!(zipfile) + dump_likes!(zipfile) + dump_bookmarks!(zipfile) + dump_actor!(zipfile) end - archive_filename = "#{['archive', Time.now.utc.strftime('%Y%m%d%H%M%S'), SecureRandom.hex(16)].join('-')}.tar.gz" + archive_filename = "#{['archive', Time.now.utc.strftime('%Y%m%d%H%M%S'), SecureRandom.hex(16)].join('-')}.zip" @backup.dump = ActionDispatch::Http::UploadedFile.new(tempfile: tmp_file, filename: archive_filename) @backup.processed = true @@ -63,27 +71,28 @@ class BackupService < BaseService tmp_file.unlink end - def dump_media_attachments!(tar) + def dump_media_attachments!(zipfile) MediaAttachment.attached.where(account: account).reorder(nil).find_in_batches do |media_attachments| media_attachments.each do |m| - next unless m.file&.path + path = m.file&.path + next unless path - download_to_tar(tar, m.file, m.file.path) + path = path.gsub(/\A.*\/system\//, '') + path = path.gsub(/\A\/+/, '') + download_to_zip(zipfile, m.file, path) end GC.start end end - def dump_outbox!(tar) - json = Oj.dump(collection) - - tar.add_file_simple('outbox.json', 0o444, json.bytesize) do |io| - io.write(json) + def dump_outbox!(zipfile) + zipfile.get_output_stream('outbox.json') do |io| + build_outbox_json!(io) end end - def dump_actor!(tar) + def dump_actor!(zipfile) actor = serialize(account, ActivityPub::ActorSerializer) actor[:icon][:url] = "avatar#{File.extname(actor[:icon][:url])}" if actor[:icon] @@ -92,51 +101,66 @@ class BackupService < BaseService actor[:likes] = 'likes.json' actor[:bookmarks] = 'bookmarks.json' - download_to_tar(tar, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists? - download_to_tar(tar, account.header, "header#{File.extname(account.header.path)}") if account.header.exists? + 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? json = Oj.dump(actor) - tar.add_file_simple('actor.json', 0o444, json.bytesize) do |io| + zipfile.get_output_stream('actor.json') do |io| io.write(json) end end - def dump_likes!(tar) - collection = serialize(ActivityPub::CollectionPresenter.new(id: 'likes.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer) + def dump_likes!(zipfile) + skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'likes.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer) + skeleton.delete(:totalItems) + skeleton[:orderedItems] = ['!PLACEHOLDER!'] + skeleton = Oj.dump(skeleton) + prepend, append = skeleton.split('"!PLACEHOLDER!"') - Status.reorder(nil).joins(:favourites).includes(:account).merge(account.favourites).find_in_batches do |statuses| - statuses.each do |status| - collection[:totalItems] += 1 - collection[:orderedItems] << ActivityPub::TagManager.instance.uri_for(status) + zipfile.get_output_stream('likes.json') do |io| + io.write(prepend) + + add_comma = false + + Status.reorder(nil).joins(:favourites).includes(:account).merge(account.favourites).find_in_batches do |statuses| + io.write(',') if add_comma + add_comma = true + + io.write(statuses.map do |status| + Oj.dump(ActivityPub::TagManager.instance.uri_for(status)) + end.join(',')) + + GC.start end - GC.start - end - - json = Oj.dump(collection) - - tar.add_file_simple('likes.json', 0o444, json.bytesize) do |io| - io.write(json) + io.write(append) end end - def dump_bookmarks!(tar) - collection = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer) + def dump_bookmarks!(zipfile) + skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer) + skeleton.delete(:totalItems) + skeleton[:orderedItems] = ['!PLACEHOLDER!'] + skeleton = Oj.dump(skeleton) + prepend, append = skeleton.split('"!PLACEHOLDER!"') - Status.reorder(nil).joins(:bookmarks).includes(:account).merge(account.bookmarks).find_in_batches do |statuses| - statuses.each do |status| - collection[:totalItems] += 1 - collection[:orderedItems] << ActivityPub::TagManager.instance.uri_for(status) + zipfile.get_output_stream('bookmarks.json') do |io| + io.write(prepend) + + add_comma = false + Status.reorder(nil).joins(:bookmarks).includes(:account).merge(account.bookmarks).find_in_batches do |statuses| + io.write(',') if add_comma + add_comma = true + + io.write(statuses.map do |status| + Oj.dump(ActivityPub::TagManager.instance.uri_for(status)) + end.join(',')) + + GC.start end - GC.start - end - - json = Oj.dump(collection) - - tar.add_file_simple('bookmarks.json', 0o444, json.bytesize) do |io| - io.write(json) + io.write(append) end end @@ -160,10 +184,10 @@ class BackupService < BaseService CHUNK_SIZE = 1.megabyte - def download_to_tar(tar, attachment, filename) + def download_to_zip(zipfile, attachment, filename) adapter = Paperclip.io_adapters.for(attachment) - tar.add_file_simple(filename, 0o444, adapter.size) do |io| + zipfile.get_output_stream(filename) do |io| while (buffer = adapter.read(CHUNK_SIZE)) io.write(buffer) end diff --git a/app/services/bulk_import_row_service.rb b/app/services/bulk_import_row_service.rb new file mode 100644 index 0000000000..4046ef4eed --- /dev/null +++ b/app/services/bulk_import_row_service.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +class BulkImportRowService + def call(row) + @account = row.bulk_import.account + @data = row.data + @type = row.bulk_import.type.to_sym + + case @type + when :following, :blocking, :muting + target_acct = @data['acct'] + target_domain = domain(target_acct) + @target_account = stoplight_wrap_request(target_domain) { ResolveAccountService.new.call(target_acct, { check_delivery_availability: true }) } + return false if @target_account.nil? + when :bookmarks + target_uri = @data['uri'] + target_domain = Addressable::URI.parse(target_uri).normalized_host + @target_status = ActivityPub::TagManager.instance.uri_to_resource(target_uri, Status) + return false if @target_status.nil? && ActivityPub::TagManager.instance.local_uri?(target_uri) + + @target_status ||= stoplight_wrap_request(target_domain) { ActivityPub::FetchRemoteStatusService.new.call(target_uri) } + return false if @target_status.nil? + end + + case @type + when :following + FollowService.new.call(@account, @target_account, reblogs: @data['show_reblogs'], notify: @data['notify'], languages: @data['languages']) + when :blocking + BlockService.new.call(@account, @target_account) + when :muting + MuteService.new.call(@account, @target_account, notifications: @data['hide_notifications']) + when :bookmarks + return false unless StatusPolicy.new(@account, @target_status).show? + + @account.bookmarks.find_or_create_by!(status: @target_status) + end + + true + rescue ActiveRecord::RecordNotFound + false + end + + def domain(uri) + domain = uri.is_a?(Account) ? uri.domain : uri.split('@')[1] + TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain) + end + + def stoplight_wrap_request(domain, &block) + if domain.present? + Stoplight("source:#{domain}", &block) + .with_fallback { nil } + .with_threshold(1) + .with_cool_off_time(5.minutes.seconds) + .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } + .run + else + yield + end + end +end diff --git a/app/services/bulk_import_service.rb b/app/services/bulk_import_service.rb new file mode 100644 index 0000000000..2701b0c7e0 --- /dev/null +++ b/app/services/bulk_import_service.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +class BulkImportService < BaseService + def call(import) + @import = import + @account = @import.account + + case @import.type.to_sym + when :following + import_follows! + when :blocking + import_blocks! + when :muting + import_mutes! + when :domain_blocking + import_domain_blocks! + when :bookmarks + import_bookmarks! + end + + @import.update!(state: :finished, finished_at: Time.now.utc) if @import.processed_items == @import.total_items + rescue + @import.update!(state: :finished, finished_at: Time.now.utc) + + raise + end + + private + + def extract_rows_by_acct + local_domain_suffix = "@#{Rails.configuration.x.local_domain}" + @import.rows.to_a.index_by { |row| row.data['acct'].delete_suffix(local_domain_suffix) } + end + + def import_follows! + rows_by_acct = extract_rows_by_acct + + if @import.overwrite? + @account.following.find_each do |followee| + row = rows_by_acct.delete(followee.acct) + + if row.nil? + UnfollowService.new.call(@account, followee) + else + row.destroy + @import.processed_items += 1 + @import.imported_items += 1 + + # Since we're updating the settings of an existing relationship, we can safely call + # FollowService directly + FollowService.new.call(@account, followee, reblogs: row.data['show_reblogs'], notify: row.data['notify'], languages: row.data['languages']) + end + end + + # Save pending infos due to `overwrite?` handling + @import.save! + end + + Import::RowWorker.push_bulk(rows_by_acct.values) do |row| + [row.id] + end + end + + def import_blocks! + rows_by_acct = extract_rows_by_acct + + if @import.overwrite? + @account.blocking.find_each do |blocked_account| + row = rows_by_acct.delete(blocked_account.acct) + + if row.nil? + UnblockService.new.call(@account, blocked_account) + else + row.destroy + @import.processed_items += 1 + @import.imported_items += 1 + BlockService.new.call(@account, blocked_account) + end + end + + # Save pending infos due to `overwrite?` handling + @import.save! + end + + Import::RowWorker.push_bulk(rows_by_acct.values) do |row| + [row.id] + end + end + + def import_mutes! + rows_by_acct = extract_rows_by_acct + + if @import.overwrite? + @account.muting.find_each do |muted_account| + row = rows_by_acct.delete(muted_account.acct) + + if row.nil? + UnmuteService.new.call(@account, muted_account) + else + row.destroy + @import.processed_items += 1 + @import.imported_items += 1 + MuteService.new.call(@account, muted_account, notifications: row.data['hide_notifications']) + end + end + + # Save pending infos due to `overwrite?` handling + @import.save! + end + + Import::RowWorker.push_bulk(rows_by_acct.values) do |row| + [row.id] + end + end + + def import_domain_blocks! + domains = @import.rows.map { |row| row.data['domain'] } + + if @import.overwrite? + @account.domain_blocks.find_each do |domain_block| + domain = domains.delete(domain_block) + + @account.unblock_domain!(domain_block.domain) if domain.nil? + end + end + + @import.rows.delete_all + domains.each { |domain| @account.block_domain!(domain) } + @import.update!(processed_items: @import.total_items, imported_items: @import.total_items) + + AfterAccountDomainBlockWorker.push_bulk(domains) do |domain| + [@account.id, domain] + end + end + + def import_bookmarks! + rows_by_uri = @import.rows.index_by { |row| row.data['uri'] } + + if @import.overwrite? + @account.bookmarks.includes(:status).find_each do |bookmark| + row = rows_by_uri.delete(ActivityPub::TagManager.instance.uri_for(bookmark.status)) + + if row.nil? + bookmark.destroy! + else + row.destroy + @import.processed_items += 1 + @import.imported_items += 1 + end + end + + # Save pending infos due to `overwrite?` handling + @import.save! + end + + Import::RowWorker.push_bulk(rows_by_uri.values) do |row| + [row.id] + end + end +end diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 8d07958b73..9c56c862ec 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -23,7 +23,7 @@ class FetchLinkCardService < BaseService @url = @original_url.to_s - with_lock("fetch:#{@original_url}") do + with_redis_lock("fetch:#{@original_url}") do @card = PreviewCard.find_by(url: @url) process_url if @card.nil? || @card.updated_at <= 2.weeks.ago || @card.missing_image? end diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb index 4470fca010..a2000e5967 100644 --- a/app/services/fetch_resource_service.rb +++ b/app/services/fetch_resource_service.rb @@ -4,6 +4,7 @@ class FetchResourceService < BaseService include JsonLdHelper ACCEPT_HEADER = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", text/html;q=0.1' + ACTIVITY_STREAM_LINK_TYPES = ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].freeze attr_reader :response_code @@ -65,7 +66,7 @@ class FetchResourceService < BaseService def process_html(response) page = Nokogiri::HTML(response.body_with_limit) - json_link = page.xpath('//link[@rel="alternate"]').find { |link| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link['type']) } + json_link = page.xpath('//link[@rel="alternate"]').find { |link| ACTIVITY_STREAM_LINK_TYPES.include?(link['type']) } process(json_link['href'], terminal: true) unless json_link.nil? end diff --git a/app/services/follow_migration_service.rb b/app/services/follow_migration_service.rb index cfe9093cbe..12ed96f984 100644 --- a/app/services/follow_migration_service.rb +++ b/app/services/follow_migration_service.rb @@ -9,10 +9,10 @@ class FollowMigrationService < FollowService def call(source_account, target_account, old_target_account, bypass_locked: false) @old_target_account = old_target_account - follow = source_account.active_relationships.find_by(target_account: old_target_account) - reblogs = follow&.show_reblogs? - notify = follow&.notify? - languages = follow&.languages + @original_follow = source_account.active_relationships.find_by(target_account: old_target_account) + reblogs = @original_follow&.show_reblogs? + notify = @original_follow&.notify? + languages = @original_follow&.languages super(source_account, target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true) end @@ -21,6 +21,7 @@ class FollowMigrationService < FollowService def request_follow! follow_request = @source_account.request_follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit])) + migrate_list_accounts! if @target_account.local? LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request') @@ -32,9 +33,30 @@ class FollowMigrationService < FollowService follow_request end + def change_follow_options! + migrate_list_accounts! + super + end + + def change_follow_request_options! + migrate_list_accounts! + super + end + def direct_follow! follow = super + + migrate_list_accounts! UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true) + follow end + + def migrate_list_accounts! + ListAccount.where(follow_id: @original_follow.id).includes(:list).find_each do |list_account| + list_account.list.accounts << @target_account + rescue ActiveRecord::RecordInvalid + nil + end + end end diff --git a/app/services/import_service.rb b/app/services/import_service.rb index f6c94cbb6f..133c081be5 100644 --- a/app/services/import_service.rb +++ b/app/services/import_service.rb @@ -2,6 +2,9 @@ require 'csv' +# NOTE: This is a deprecated service, only kept to not break ongoing imports +# on upgrade. See `BulkImportService` for its replacement. + class ImportService < BaseService ROWS_PROCESSING_LIMIT = 20_000 diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 4f98ccea70..e1c1c35fcf 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -18,7 +18,7 @@ class RemoveStatusService < BaseService @account = status.account @options = options - with_lock("distribute:#{@status.id}") do + with_redis_lock("distribute:#{@status.id}") do @status.discard_with_reblogs StatusPin.find_by(status: @status)&.destroy diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index abe1534a55..6204fefd6f 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -100,13 +100,13 @@ class ResolveAccountService < BaseService end def split_acct(acct) - acct.gsub(/\Aacct:/, '').split('@') + acct.delete_prefix('acct:').split('@') end def fetch_account! return unless activitypub_ready? - with_lock("resolve:#{@username}@#{@domain}") do + with_redis_lock("resolve:#{@username}@#{@domain}") do @account = ActivityPub::FetchRemoteAccountService.new.call(actor_url, suppress_errors: @options[:suppress_errors]) end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index cfb3eb5831..e9e23a219b 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -68,7 +68,7 @@ class SuspendAccountService < BaseService @account.media_attachments.find_each do |media_attachment| attachment_names.each do |attachment_name| attachment = media_attachment.public_send(attachment_name) - styles = [:original] | attachment.styles.keys + styles = MediaAttachment::DEFAULT_STYLES | attachment.styles.keys next if attachment.blank? diff --git a/app/services/tag_search_service.rb b/app/services/tag_search_service.rb index b66ccced9d..d5d1974275 100644 --- a/app/services/tag_search_service.rb +++ b/app/services/tag_search_service.rb @@ -2,7 +2,7 @@ class TagSearchService < BaseService def call(query, options = {}) - @query = query.strip.gsub(/\A#/, '') + @query = query.strip.delete_prefix('#') @offset = options.delete(:offset).to_i @limit = options.delete(:limit).to_i @options = options diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index d83a60e4e7..fe9a7f0d87 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -15,7 +15,7 @@ class UnfollowService < BaseService @target_account = target_account @options = options - with_lock("relationship:#{[source_account.id, target_account.id].sort.join(':')}") do + with_redis_lock("relationship:#{[source_account.id, target_account.id].sort.join(':')}") do unfollow! || undo_follow_request! end end diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb index d851a0f708..51665eab94 100644 --- a/app/services/unsuspend_account_service.rb +++ b/app/services/unsuspend_account_service.rb @@ -64,7 +64,7 @@ class UnsuspendAccountService < BaseService @account.media_attachments.find_each do |media_attachment| attachment_names.each do |attachment_name| attachment = media_attachment.public_send(attachment_name) - styles = [:original] | attachment.styles.keys + styles = MediaAttachment::DEFAULT_STYLES | attachment.styles.keys next if attachment.blank? diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb index 9ebf5a98d9..3e92a1690a 100644 --- a/app/services/vote_service.rb +++ b/app/services/vote_service.rb @@ -18,7 +18,7 @@ class VoteService < BaseService already_voted = true - with_lock("vote:#{@poll.id}:#{@account.id}") do + with_redis_lock("vote:#{@poll.id}:#{@account.id}") do already_voted = @poll.votes.where(account: @account).exists? ApplicationRecord.transaction do diff --git a/app/validators/email_mx_validator.rb b/app/validators/email_mx_validator.rb index 19c57bdf66..a30a0c820d 100644 --- a/app/validators/email_mx_validator.rb +++ b/app/validators/email_mx_validator.rb @@ -8,9 +8,7 @@ class EmailMxValidator < ActiveModel::Validator domain = get_domain(user.email) - if domain.blank? - user.errors.add(:email, :invalid) - elsif domain.include?('..') + if domain.blank? || domain.include?('..') user.errors.add(:email, :invalid) elsif !on_allowlist?(domain) resolved_ips, resolved_domains = resolve_mx(domain) diff --git a/app/validators/import_validator.rb b/app/validators/import_validator.rb deleted file mode 100644 index 782baf5d6b..0000000000 --- a/app/validators/import_validator.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'csv' - -class ImportValidator < ActiveModel::Validator - KNOWN_HEADERS = [ - 'Account address', - '#domain', - '#uri', - ].freeze - - def validate(import) - return if import.type.blank? || import.data.blank? - - # We parse because newlines could be part of individual rows. This - # runs on create so we should be reading the local file here before - # it is uploaded to object storage or moved anywhere... - csv_data = CSV.parse(import.data.queued_for_write[:original].read) - - row_count = csv_data.size - row_count -= 1 if KNOWN_HEADERS.include?(csv_data.first&.first) - - import.errors.add(:data, I18n.t('imports.errors.over_rows_processing_limit', count: ImportService::ROWS_PROCESSING_LIMIT)) if row_count > ImportService::ROWS_PROCESSING_LIMIT - - case import.type - when 'following' - validate_following_import(import, row_count) - end - rescue CSV::MalformedCSVError - import.errors.add(:data, :malformed) - end - - private - - def validate_following_import(import, row_count) - base_limit = FollowLimitValidator.limit_for_account(import.account) - - limit = if import.overwrite? - base_limit - else - base_limit - import.account.following_count - end - - import.errors.add(:data, I18n.t('users.follow_limit_reached', limit: base_limit)) if row_count > limit - end -end diff --git a/app/validators/vote_validator.rb b/app/validators/vote_validator.rb index 9c55f9ab6d..9bd17fbe80 100644 --- a/app/validators/vote_validator.rb +++ b/app/validators/vote_validator.rb @@ -6,15 +6,23 @@ class VoteValidator < ActiveModel::Validator vote.errors.add(:base, I18n.t('polls.errors.invalid_choice')) if invalid_choice?(vote) - if vote.poll_multiple? && already_voted_for_same_choice_on_multiple_poll?(vote) - vote.errors.add(:base, I18n.t('polls.errors.already_voted')) - elsif !vote.poll_multiple? && already_voted_on_non_multiple_poll?(vote) - vote.errors.add(:base, I18n.t('polls.errors.already_voted')) - end + vote.errors.add(:base, I18n.t('polls.errors.already_voted')) if additional_voting_not_allowed?(vote) end private + def additional_voting_not_allowed?(vote) + poll_multiple_and_already_voted?(vote) || poll_non_multiple_and_already_voted?(vote) + end + + def poll_multiple_and_already_voted?(vote) + vote.poll_multiple? && already_voted_for_same_choice_on_multiple_poll?(vote) + end + + def poll_non_multiple_and_already_voted?(vote) + !vote.poll_multiple? && already_voted_on_non_multiple_poll?(vote) + end + def invalid_choice?(vote) vote.choice.negative? || vote.choice >= vote.poll.options.size end diff --git a/app/views/settings/imports/index.html.haml b/app/views/settings/imports/index.html.haml new file mode 100644 index 0000000000..02c3f4eb3f --- /dev/null +++ b/app/views/settings/imports/index.html.haml @@ -0,0 +1,49 @@ +- content_for :page_title do + = t('settings.import') + += simple_form_for @import, url: settings_imports_path do |f| + .field-group + = f.input :type, as: :grouped_select, collection: { constructive: %i(following bookmarks), destructive: %i(muting blocking domain_blocking) }, wrapper: :with_block_label, include_blank: false, label_method: ->(type) { I18n.t("imports.types.#{type}") }, group_label_method: ->(group) { I18n.t("imports.type_groups.#{group.first}") }, group_method: :last, hint: t('imports.preface') + + .fields-row + .fields-group.fields-row__column.fields-row__column-6 + = f.input :data, as: :file, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data') + .fields-group.fields-row__column.fields-row__column-6 + = f.input :mode, as: :radio_buttons, collection: Import::MODES, label_method: ->(mode) { safe_join([I18n.t("imports.modes.#{mode}"), content_tag(:span, I18n.t("imports.modes.#{mode}_long"), class: 'hint')]) }, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + + .actions + = f.button :button, t('imports.upload'), type: :submit + +- unless @recent_imports.empty? + %hr.spacer/ + + %h3= t('imports.recent_imports') + + .table-wrapper + %table.table + %thead + %tr + %th= t('imports.type') + %th= t('imports.status') + %th= t('imports.imported') + %th= t('imports.time_started') + %th= t('imports.failures') + %tbody + - @recent_imports.each do |import| + %tr + %td= t("imports.types.#{import.type}") + %td + - if import.unconfirmed? + = link_to t("imports.states.#{import.state}"), settings_import_path(import) + - else + = t("imports.states.#{import.state}") + %td + #{import.imported_items} / #{import.total_items} + %td= l(import.created_at) + %td + - num_failed = import.processed_items - import.imported_items + - if num_failed.positive? + - if import.finished? + = link_to num_failed, failures_settings_import_path(import, format: 'csv') + - else + = num_failed diff --git a/app/views/settings/imports/show.html.haml b/app/views/settings/imports/show.html.haml index 7bb4beb01c..65954e3e1e 100644 --- a/app/views/settings/imports/show.html.haml +++ b/app/views/settings/imports/show.html.haml @@ -1,15 +1,15 @@ - content_for :page_title do - = t('settings.import') + = t("imports.titles.#{@bulk_import.type.to_s}") -= simple_form_for @import, url: settings_import_path do |f| - .field-group - = f.input :type, collection: Import.types.keys, wrapper: :with_block_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, hint: t('imports.preface') +- if @bulk_import.likely_mismatched? + .flash-message.warning= t("imports.mismatched_types_warning") - .fields-row - .fields-group.fields-row__column.fields-row__column-6 - = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data') - .fields-group.fields-row__column.fields-row__column-6 - = f.input :mode, as: :radio_buttons, collection: Import::MODES, label_method: lambda { |mode| safe_join([I18n.t("imports.modes.#{mode}"), content_tag(:span, I18n.t("imports.modes.#{mode}_long"), class: 'hint')]) }, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' +- if @bulk_import.overwrite? + %p.hint= t("imports.overwrite_preambles.#{@bulk_import.type.to_s}_html", filename: @bulk_import.original_filename, total_items: @bulk_import.total_items) +- else + %p.hint= t("imports.preambles.#{@bulk_import.type.to_s}_html", filename: @bulk_import.original_filename, total_items: @bulk_import.total_items) +.simple_form .actions - = f.button :button, t('imports.upload'), type: :submit + = link_to t('generic.cancel'), settings_import_path(@bulk_import), method: :delete, class: 'button button-tertiary' + = link_to t('generic.confirm'), confirm_settings_import_path(@bulk_import), method: :post, class: 'button' diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb new file mode 100644 index 0000000000..54571a95c0 --- /dev/null +++ b/app/workers/bulk_import_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class BulkImportWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull', retry: false + + def perform(import_id) + import = BulkImport.find(import_id) + import.update!(state: :in_progress) + BulkImportService.new.call(import) + end +end diff --git a/app/workers/distribution_worker.rb b/app/workers/distribution_worker.rb index 59cdbc7b25..1d58e53e94 100644 --- a/app/workers/distribution_worker.rb +++ b/app/workers/distribution_worker.rb @@ -6,7 +6,7 @@ class DistributionWorker include Lockable def perform(status_id, options = {}) - with_lock("distribute:#{status_id}") do + with_redis_lock("distribute:#{status_id}") do FanOutOnWriteService.new.call(Status.find(status_id), **options.symbolize_keys) end rescue ActiveRecord::RecordNotFound diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb index c2728c1961..a411ab5876 100644 --- a/app/workers/import/relationship_worker.rb +++ b/app/workers/import/relationship_worker.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +# NOTE: This is a deprecated worker, only kept to not break ongoing imports +# on upgrade. See `Import::RowWorker` for its replacement. + class Import::RelationshipWorker include Sidekiq::Worker diff --git a/app/workers/import/row_worker.rb b/app/workers/import/row_worker.rb new file mode 100644 index 0000000000..09dd6ce736 --- /dev/null +++ b/app/workers/import/row_worker.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Import::RowWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull', retry: 6, dead: false + + sidekiq_retries_exhausted do |msg, _exception| + ActiveRecord::Base.connection_pool.with_connection do + # Increment the total number of processed items, and bump the state of the import if needed + bulk_import_id = BulkImportRow.where(id: msg['args'][0]).pick(:id) + BulkImport.progress!(bulk_import_id) unless bulk_import_id.nil? + end + end + + def perform(row_id) + row = BulkImportRow.eager_load(bulk_import: :account).find_by(id: row_id) + return true if row.nil? + + imported = BulkImportRowService.new.call(row) + + mark_as_processed!(row, imported) + end + + private + + def mark_as_processed!(row, imported) + bulk_import_id = row.bulk_import_id + row.destroy! if imported + + BulkImport.progress!(bulk_import_id, imported: imported) + end +end diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb index dfa71b29ec..b6afb972a9 100644 --- a/app/workers/import_worker.rb +++ b/app/workers/import_worker.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +# NOTE: This is a deprecated worker, only kept to not break ongoing imports +# on upgrade. See `ImportWorker` for its replacement. + class ImportWorker include Sidekiq::Worker diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb index 3b429928ea..cb091671df 100644 --- a/app/workers/move_worker.rb +++ b/app/workers/move_worker.rb @@ -8,9 +8,9 @@ class MoveWorker @target_account = Account.find(target_account_id) if @target_account.local? && @source_account.local? - nb_moved = rewrite_follows! - @source_account.update_count!(:followers_count, -nb_moved) - @target_account.update_count!(:followers_count, nb_moved) + num_moved = rewrite_follows! + @source_account.update_count!(:followers_count, -num_moved) + @target_account.update_count!(:followers_count, num_moved) else queue_follow_unfollows! end @@ -29,12 +29,44 @@ class MoveWorker private def rewrite_follows! + num_moved = 0 + + # First, approve pending follow requests for the new account, + # this allows correctly processing list memberships with pending + # follow requests + FollowRequest.where(account: @source_account.followers, target_account_id: @target_account.id).find_each do |follow_request| + ListAccount.where(follow_id: follow_request.id).includes(:list).find_each do |list_account| + list_account.list.accounts << @target_account + rescue ActiveRecord::RecordInvalid + nil + end + + follow_request.authorize! + end + + # Then handle accounts that follow both the old and new account + @source_account.passive_relationships + .where(account: Account.local) + .where(account: @target_account.followers.local) + .in_batches do |follows| + ListAccount.where(follow: follows).includes(:list).find_each do |list_account| + list_account.list.accounts << @target_account + rescue ActiveRecord::RecordInvalid + nil + end + end + + # Finally, handle the common case of accounts not following the new account @source_account.passive_relationships .where(account: Account.local) .where.not(account: @target_account.followers.local) .where.not(account_id: @target_account.id) - .in_batches - .update_all(target_account_id: @target_account.id) + .in_batches do |follows| + ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id) + num_moved += follows.update_all(target_account_id: @target_account.id) + end + + num_moved end def queue_follow_unfollows! diff --git a/app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb b/app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb index a7737622db..a2ab31cc5d 100644 --- a/app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb +++ b/app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb @@ -38,17 +38,37 @@ class Scheduler::AccountsStatusesCleanupScheduler return if under_load? budget = compute_budget - first_policy_id = last_processed_id + + # If the budget allows it, we want to consider all accounts with enabled + # auto cleanup at least once. + # + # We start from `first_policy_id` (the last processed id in the previous + # run) and process each policy until we loop to `first_policy_id`, + # recording into `affected_policies` any policy that caused posts to be + # deleted. + # + # After that, we set `full_iteration` to `false` and continue looping on + # policies from `affected_policies`. + first_policy_id = last_processed_id || 0 + first_iteration = true + full_iteration = true + affected_policies = [] loop do num_processed_accounts = 0 - scope = AccountStatusesCleanupPolicy.where(enabled: true) - scope = scope.where(id: first_policy_id...) if first_policy_id.present? + scope = cleanup_policies(first_policy_id, affected_policies, first_iteration, full_iteration) scope.find_each(order: :asc) do |policy| num_deleted = AccountStatusesCleanupService.new.call(policy, [budget, PER_ACCOUNT_BUDGET].min) - num_processed_accounts += 1 unless num_deleted.zero? budget -= num_deleted + + unless num_deleted.zero? + num_processed_accounts += 1 + affected_policies << policy.id if full_iteration + end + + full_iteration = false if !first_iteration && policy.id >= first_policy_id + if budget.zero? save_last_processed_id(policy.id) break @@ -57,9 +77,10 @@ class Scheduler::AccountsStatusesCleanupScheduler # The idea here is to loop through all policies at least once until the budget is exhausted # and start back after the last processed account otherwise - break if budget.zero? || (num_processed_accounts.zero? && first_policy_id.nil?) + break if budget.zero? || (num_processed_accounts.zero? && !full_iteration) - first_policy_id = nil + full_iteration = false unless first_iteration + first_iteration = false end end @@ -76,12 +97,28 @@ class Scheduler::AccountsStatusesCleanupScheduler private + def cleanup_policies(first_policy_id, affected_policies, first_iteration, full_iteration) + scope = AccountStatusesCleanupPolicy.where(enabled: true) + + if full_iteration + # If we are doing a full iteration, examine all policies we have not examined yet + if first_iteration + scope.where(id: first_policy_id...) + else + scope.where(id: ..first_policy_id).or(scope.where(id: affected_policies)) + end + else + # Otherwise, examine only policies that previously yielded posts to delete + scope.where(id: affected_policies) + end + end + def queue_under_load?(name, max_latency) Sidekiq::Queue.new(name).latency > max_latency end def last_processed_id - redis.get('account_statuses_cleanup_scheduler:last_policy_id') + redis.get('account_statuses_cleanup_scheduler:last_policy_id')&.to_i end def save_last_processed_id(id) diff --git a/app/workers/scheduler/vacuum_scheduler.rb b/app/workers/scheduler/vacuum_scheduler.rb index 9544f808bf..9e884caefd 100644 --- a/app/workers/scheduler/vacuum_scheduler.rb +++ b/app/workers/scheduler/vacuum_scheduler.rb @@ -23,6 +23,7 @@ class Scheduler::VacuumScheduler backups_vacuum, access_tokens_vacuum, feeds_vacuum, + imports_vacuum, ] end @@ -50,6 +51,10 @@ class Scheduler::VacuumScheduler Vacuum::FeedsVacuum.new end + def imports_vacuum + Vacuum::ImportsVacuum.new + end + def content_retention_policy ContentRetentionPolicy.current end diff --git a/babel.config.js b/babel.config.js index 0b81f1453f..abfdc5b2ca 100644 --- a/babel.config.js +++ b/babel.config.js @@ -9,6 +9,9 @@ module.exports = (api) => { loose: true, modules: false, debug: false, + include: [ + 'proposal-numeric-separator', + ], }; const config = { diff --git a/config/deploy.rb b/config/deploy.rb index b69a830674..b19567a729 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -lock '3.17.1' +lock '3.17.2' set :repo_url, ENV.fetch('REPO', 'https://github.com/mastodon/mastodon.git') set :branch, ENV.fetch('BRANCH', 'main') @@ -13,9 +13,12 @@ set :migration_role, :app append :linked_files, '.env.production', 'public/robots.txt' append :linked_dirs, 'vendor/bundle', 'node_modules', 'public/system' +SYSTEMD_SERVICES = %i[sidekiq streaming web].freeze +SERVICE_ACTIONS = %i[reload restart status].freeze + namespace :systemd do - %i[sidekiq streaming web].each do |service| - %i[reload restart status].each do |action| + SYSTEMD_SERVICES.each do |service| + SERVICE_ACTIONS.each do |action| desc "Perform a #{action} on #{service} service" task "#{service}:#{action}".to_sym do on roles(:app) do diff --git a/config/environments/development.rb b/config/environments/development.rb index a633dfce51..306324c046 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -14,7 +14,7 @@ Rails.application.configure do # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. - if Rails.root.join('tmp/caching-dev.txt').exist? + if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS else diff --git a/config/environments/test.rb b/config/environments/test.rb index 493b041ebb..08cc4c4d3c 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -59,7 +59,7 @@ Rails.application.configure do end end -Paperclip::Attachment.default_options[:path] = "#{Rails.root}/spec/test_files/:class/:id_partition/:style.:extension" +Paperclip::Attachment.default_options[:path] = Rails.root.join('spec', 'test_files', ':class', ':id_partition', ':style.:extension') # set fake_data for pam, don't do real calls, just use fake data if ENV['PAM_ENABLED'] == 'true' diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index a3b5e5c724..92e1c8c7b0 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -72,6 +72,8 @@ ignore_unused: - 'themes.*' - 'move_handler.carry_{mutes,blocks}_over_text' - 'notification_mailer.*' + - 'imports.overwrite_preambles.{following,blocking,muting,domain_blocking,bookmarks}_html' + - 'imports.preambles.{following,blocking,muting,domain_blocking,bookmarks}_html' ignore_inconsistent_interpolations: - '*.one' diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index b72cbe1927..9f0ffc6dc7 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -68,11 +68,7 @@ if ENV['S3_ENABLED'] == 'true' } ) - if ENV['S3_PERMISSION'] == '' - Paperclip::Attachment.default_options.merge!( - s3_permissions: ->(*) { nil } - ) - end + Paperclip::Attachment.default_options[:s3_permissions] = ->(*) { nil } if ENV['S3_PERMISSION'] == '' if ENV.has_key?('S3_ENDPOINT') Paperclip::Attachment.default_options[:s3_options].merge!( @@ -90,11 +86,7 @@ if ENV['S3_ENABLED'] == 'true' ) end - if ENV.has_key?('S3_STORAGE_CLASS') - Paperclip::Attachment.default_options[:s3_headers].merge!( - 'X-Amz-Storage-Class' => ENV['S3_STORAGE_CLASS'] - ) - end + Paperclip::Attachment.default_options[:s3_headers]['X-Amz-Storage-Class'] = ENV['S3_STORAGE_CLASS'] if ENV.has_key?('S3_STORAGE_CLASS') # Some S3-compatible providers might not actually be compatible with some APIs # used by kt-paperclip, see https://github.com/mastodon/mastodon/issues/16822 diff --git a/config/locales/en.yml b/config/locales/en.yml index ddefbc49b1..0188519c26 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1218,7 +1218,9 @@ en: all_matching_items_selected_html: one: "<strong>%{count}</strong> item matching your search is selected." other: All <strong>%{count}</strong> items matching your search are selected. + cancel: Cancel changes_saved_msg: Changes successfully saved! + confirm: Confirm copy: Copy delete: Delete deselect: Deselect all @@ -1234,15 +1236,51 @@ en: other: Something isn't quite right yet! Please review %{count} errors below imports: errors: + empty: Empty CSV file + incompatible_type: Incompatible with the selected import type invalid_csv_file: 'Invalid CSV file. Error: %{error}' over_rows_processing_limit: contains more than %{count} rows + too_large: File is too large + failures: Failures + imported: Imported + mismatched_types_warning: It appears you may have selected the wrong type for this import, please double-check. modes: merge: Merge merge_long: Keep existing records and add new ones overwrite: Overwrite overwrite_long: Replace current records with the new ones + overwrite_preambles: + blocking_html: You are about to <strong>replace your block list</strong> with up to <strong>%{total_items} accounts</strong> from <strong>%{filename}</strong>. + bookmarks_html: You are about to <strong>replace your bookmarks</strong> with up to <strong>%{total_items} posts</strong> from <strong>%{filename}</strong>. + domain_blocking_html: You are about to <strong>replace your domain block list</strong> with up to <strong>%{total_items} domains</strong> from <strong>%{filename}</strong>. + following_html: You are about to <strong>follow</strong> up to <strong>%{total_items} accounts</strong> from <strong>%{filename}</strong> and <strong>stop following anyone else</strong>. + muting_html: You are about to <strong>replace your list of muted accounts</strong> with up to <strong>%{total_items} accounts</strong> from <strong>%{filename}</strong>. + preambles: + blocking_html: You are about to <strong>block</strong> up to <strong>%{total_items} accounts</strong> from <strong>%{filename}</strong>. + bookmarks_html: You are about to add up to <strong>%{total_items} posts</strong> from <strong>%{filename}</strong> to your <strong>bookmarks</strong>. + domain_blocking_html: You are about to <strong>block</strong> up to <strong>%{total_items} domains</strong> from <strong>%{filename}</strong>. + following_html: You are about to <strong>follow</strong> up to <strong>%{total_items} accounts</strong> from <strong>%{filename}</strong>. + muting_html: You are about to <strong>mute</strong> up to <strong>%{total_items} accounts</strong> from <strong>%{filename}</strong>. preface: You can import data that you have exported from another server, such as a list of the people you are following or blocking. + recent_imports: Recent imports + states: + finished: Finished + in_progress: In progress + scheduled: Scheduled + unconfirmed: Unconfirmed + status: Status success: Your data was successfully uploaded and will be processed in due time + time_started: Started at + titles: + blocking: Importing blocked accounts + bookmarks: Importing bookmarks + domain_blocking: Importing blocked domains + following: Importing followed accounts + muting: Importing muted accounts + type: Import type + type_groups: + constructive: Follows & Bookmarks + destructive: Blocks & mutes types: blocking: Blocking list bookmarks: Bookmarks diff --git a/config/navigation.rb b/config/navigation.rb index aab72d27c4..d6a8d13690 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -32,7 +32,7 @@ SimpleNavigation::Configuration.run do |navigation| end n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_path do |s| - s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_path, if: -> { current_user.functional? } + s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_imports_path, if: -> { current_user.functional? } s.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_path end diff --git a/config/routes.rb b/config/routes.rb index 89f3fd18b9..3286d16106 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -133,73 +133,7 @@ Rails.application.routes.draw do get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: /([^\/])+?/ }, format: false get '/settings', to: redirect('/settings/profile') - namespace :settings do - resource :profile, only: [:show, :update] do - resources :pictures, only: :destroy - end - - get :preferences, to: redirect('/settings/preferences/appearance') - - namespace :preferences do - resource :appearance, only: [:show, :update], controller: :appearance - resource :notifications, only: [:show, :update] - resource :other, only: [:show, :update], controller: :other - end - - resource :import, only: [:show, :create] - resource :export, only: [:show, :create] - - namespace :exports, constraints: { format: :csv } do - resources :follows, only: :index, controller: :following_accounts - resources :blocks, only: :index, controller: :blocked_accounts - resources :mutes, only: :index, controller: :muted_accounts - resources :lists, only: :index, controller: :lists - resources :domain_blocks, only: :index, controller: :blocked_domains - resources :bookmarks, only: :index, controller: :bookmarks - end - - resources :two_factor_authentication_methods, only: [:index] do - collection do - post :disable - end - end - - resource :otp_authentication, only: [:show, :create], controller: 'two_factor_authentication/otp_authentication' - - resources :webauthn_credentials, only: [:index, :new, :create, :destroy], - path: 'security_keys', - controller: 'two_factor_authentication/webauthn_credentials' do - - collection do - get :options - end - end - - namespace :two_factor_authentication do - resources :recovery_codes, only: [:create] - resource :confirmation, only: [:new, :create] - end - - resources :applications, except: [:edit] do - member do - post :regenerate - end - end - - resources :flavours, only: [:index, :show, :update], param: :flavour - - resource :delete, only: [:show, :destroy] - resource :migration, only: [:show, :create] - - namespace :migration do - resource :redirect, only: [:new, :create, :destroy] - end - - resources :aliases, only: [:index, :create, :destroy] - resources :sessions, only: [:destroy] - resources :featured_tags, only: [:index, :create, :destroy] - resources :login_activities, only: [:index] - end + draw(:settings) namespace :disputes do resources :strikes, only: [:show, :index] do @@ -231,516 +165,11 @@ Rails.application.routes.draw do resource :authorize_interaction, only: [:show, :create] resource :share, only: [:show, :create] - namespace :admin do - get '/dashboard', to: 'dashboard#index' - - resources :domain_allows, only: [:new, :create, :show, :destroy] - resources :domain_blocks, only: [:new, :create, :destroy, :update, :edit] do - collection do - post :batch - end - end - - resources :export_domain_allows, only: [:new] do - collection do - get :export, constraints: { format: :csv } - post :import - end - end - - resources :export_domain_blocks, only: [:new] do - collection do - get :export, constraints: { format: :csv } - post :import - end - end - - resources :email_domain_blocks, only: [:index, :new, :create] do - collection do - post :batch - end - end - - resources :action_logs, only: [:index] - resources :warning_presets, except: [:new] - - resources :announcements, except: [:show] do - member do - post :publish - post :unpublish - end - end - - get '/settings', to: redirect('/admin/settings/branding') - get '/settings/edit', to: redirect('/admin/settings/branding') - - namespace :settings do - resource :branding, only: [:show, :update], controller: 'branding' - resource :registrations, only: [:show, :update], controller: 'registrations' - resource :content_retention, only: [:show, :update], controller: 'content_retention' - resource :about, only: [:show, :update], controller: 'about' - resource :appearance, only: [:show, :update], controller: 'appearance' - resource :discovery, only: [:show, :update], controller: 'discovery' - resource :other, only: [:show, :update], controller: 'other' - end - - resources :site_uploads, only: [:destroy] - - resources :invites, only: [:index, :create, :destroy] do - collection do - post :deactivate_all - end - end - - resources :relays, only: [:index, :new, :create, :destroy] do - member do - post :enable - post :disable - end - end - - resources :instances, only: [:index, :show, :destroy], constraints: { id: /[^\/]+/ } do - member do - post :clear_delivery_errors - post :restart_delivery - post :stop_delivery - end - end - - resources :rules - - resources :webhooks do - member do - post :enable - post :disable - end - - resource :secret, only: [], controller: 'webhooks/secrets' do - post :rotate - end - end - - resources :reports, only: [:index, :show] do - resources :actions, only: [:create], controller: 'reports/actions' do - collection do - post :preview - end - end - - member do - post :assign_to_self - post :unassign - post :reopen - post :resolve - end - end - - resources :report_notes, only: [:create, :destroy] - - resources :accounts, only: [:index, :show, :destroy] do - member do - post :enable - post :unsensitive - post :unsilence - post :unsuspend - post :redownload - post :remove_avatar - post :remove_header - post :memorialize - post :approve - post :reject - post :unblock_email - end - - collection do - post :batch - end - - resource :change_email, only: [:show, :update] - resource :reset, only: [:create] - resource :action, only: [:new, :create], controller: 'account_actions' - - resources :statuses, only: [:index, :show] do - collection do - post :batch - end - end - - resources :relationships, only: [:index] - - resource :confirmation, only: [:create] do - collection do - post :resend - end - end - end - - resources :users, only: [] do - resource :two_factor_authentication, only: [:destroy], controller: 'users/two_factor_authentications' - resource :role, only: [:show, :update], controller: 'users/roles' - end - - resources :custom_emojis, only: [:index, :new, :create] do - collection do - post :batch - end - end - - resources :ip_blocks, only: [:index, :new, :create] do - collection do - post :batch - end - end - - resources :roles, except: [:show] - resources :account_moderation_notes, only: [:create, :destroy] - resource :follow_recommendations, only: [:show, :update] - resources :tags, only: [:show, :update] - - namespace :trends do - resources :links, only: [:index] do - collection do - post :batch - end - end - - resources :tags, only: [:index] do - collection do - post :batch - end - end - - resources :statuses, only: [:index] do - collection do - post :batch - end - end - - namespace :links do - resources :preview_card_providers, only: [:index], path: :publishers do - collection do - post :batch - end - end - end - end - - namespace :disputes do - resources :appeals, only: [:index] do - member do - post :approve - post :reject - end - end - end - end + draw(:admin) get '/admin', to: redirect('/admin/dashboard', status: 302) - namespace :api, format: false do - # OEmbed - get '/oembed', to: 'oembed#show', as: :oembed - - # JSON / REST API - namespace :v1 do - resources :statuses, only: [:create, :show, :update, :destroy] do - scope module: :statuses do - resources :reblogged_by, controller: :reblogged_by_accounts, only: :index - resources :favourited_by, controller: :favourited_by_accounts, only: :index - resource :reblog, only: :create - post :unreblog, to: 'reblogs#destroy' - - resource :favourite, only: :create - post :unfavourite, to: 'favourites#destroy' - - resource :bookmark, only: :create - post :unbookmark, to: 'bookmarks#destroy' - - resource :mute, only: :create - post :unmute, to: 'mutes#destroy' - - resource :pin, only: :create - post :unpin, to: 'pins#destroy' - - resource :history, only: :show - resource :source, only: :show - - post :translate, to: 'translations#create' - end - - member do - get :context - end - end - - namespace :timelines do - resource :direct, only: :show, controller: :direct - resource :home, only: :show, controller: :home - resource :public, only: :show, controller: :public - resources :tag, only: :show - resources :list, only: :show - end - - get '/streaming', to: 'streaming#index' - get '/streaming/(*any)', to: 'streaming#index' - - resources :custom_emojis, only: [:index] - resources :suggestions, only: [:index, :destroy] - resources :scheduled_statuses, only: [:index, :show, :update, :destroy] - resources :preferences, only: [:index] - - resources :announcements, only: [:index] do - scope module: :announcements do - resources :reactions, only: [:update, :destroy] - end - - member do - post :dismiss - end - end - - # namespace :crypto do - # resources :deliveries, only: :create - - # namespace :keys do - # resource :upload, only: [:create] - # resource :query, only: [:create] - # resource :claim, only: [:create] - # resource :count, only: [:show] - # end - - # resources :encrypted_messages, only: [:index] do - # collection do - # post :clear - # end - # end - # end - - resources :conversations, only: [:index, :destroy] do - member do - post :read - end - end - - resources :media, only: [:create, :update, :show] - resources :blocks, only: [:index] - resources :mutes, only: [:index] - resources :favourites, only: [:index] - resources :bookmarks, only: [:index] - resources :reports, only: [:create] - resources :trends, only: [:index], controller: 'trends/tags' - resources :filters, only: [:index, :create, :show, :update, :destroy] - resources :endorsements, only: [:index] - resources :markers, only: [:index, :create] - - namespace :apps do - get :verify_credentials, to: 'credentials#show' - end - - resources :apps, only: [:create] - - namespace :trends do - resources :links, only: [:index] - resources :tags, only: [:index] - resources :statuses, only: [:index] - end - - namespace :emails do - resources :confirmations, only: [:create] - end - - resource :instance, only: [:show] do - resources :peers, only: [:index], controller: 'instances/peers' - resources :rules, only: [:index], controller: 'instances/rules' - resources :domain_blocks, only: [:index], controller: 'instances/domain_blocks' - resource :privacy_policy, only: [:show], controller: 'instances/privacy_policies' - resource :extended_description, only: [:show], controller: 'instances/extended_descriptions' - resource :translation_languages, only: [:show], controller: 'instances/translation_languages' - resource :activity, only: [:show], controller: 'instances/activity' - end - - resource :domain_blocks, only: [:show, :create, :destroy] - - resource :directory, only: [:show] - - resources :follow_requests, only: [:index] do - member do - post :authorize - post :reject - end - end - - resources :notifications, only: [:index, :show, :destroy] do - collection do - post :clear - delete :destroy_multiple - end - - member do - post :dismiss - end - end - - namespace :accounts do - get :verify_credentials, to: 'credentials#show' - patch :update_credentials, to: 'credentials#update' - resource :search, only: :show, controller: :search - resource :lookup, only: :show, controller: :lookup - resources :relationships, only: :index - resources :familiar_followers, only: :index - end - - resources :accounts, only: [:create, :show] do - resources :statuses, only: :index, controller: 'accounts/statuses' - resources :followers, only: :index, controller: 'accounts/follower_accounts' - resources :following, only: :index, controller: 'accounts/following_accounts' - resources :lists, only: :index, controller: 'accounts/lists' - resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs' - resources :featured_tags, only: :index, controller: 'accounts/featured_tags' - - member do - post :follow - post :unfollow - post :remove_from_followers - post :block - post :unblock - post :mute - post :unmute - end - - resource :pin, only: :create, controller: 'accounts/pins' - post :unpin, to: 'accounts/pins#destroy' - resource :note, only: :create, controller: 'accounts/notes' - end - - resources :tags, only: [:show] do - member do - post :follow - post :unfollow - end - end - - resources :followed_tags, only: [:index] - - resources :lists, only: [:index, :create, :show, :update, :destroy] do - resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts' - end - - namespace :featured_tags do - get :suggestions, to: 'suggestions#index' - end - - resources :featured_tags, only: [:index, :create, :destroy] - - resources :polls, only: [:create, :show] do - resources :votes, only: :create, controller: 'polls/votes' - end - - namespace :push do - resource :subscription, only: [:create, :show, :update, :destroy] - end - - namespace :admin do - resources :accounts, only: [:index, :show, :destroy] do - member do - post :enable - post :unsensitive - post :unsilence - post :unsuspend - post :approve - post :reject - end - - resource :action, only: [:create], controller: 'account_actions' - end - - resources :reports, only: [:index, :update, :show] do - member do - post :assign_to_self - post :unassign - post :reopen - post :resolve - end - end - - resources :domain_allows, only: [:index, :show, :create, :destroy] - resources :domain_blocks, only: [:index, :show, :update, :create, :destroy] - resources :email_domain_blocks, only: [:index, :show, :create, :destroy] - resources :ip_blocks, only: [:index, :show, :update, :create, :destroy] - - namespace :trends do - resources :tags, only: [:index] do - member do - post :approve - post :reject - end - end - resources :links, only: [:index] do - member do - post :approve - post :reject - end - end - resources :statuses, only: [:index] do - member do - post :approve - post :reject - end - end - - namespace :links do - resources :preview_card_providers, only: [:index], path: :publishers do - member do - post :approve - post :reject - end - end - end - end - - post :measures, to: 'measures#create' - post :dimensions, to: 'dimensions#create' - post :retention, to: 'retention#create' - - resources :canonical_email_blocks, only: [:index, :create, :show, :destroy] do - collection do - post :test - end - end - end - end - - namespace :v2 do - get '/search', to: 'search#index', as: :search - - resources :media, only: [:create] - resources :suggestions, only: [:index] - resource :instance, only: [:show] - resources :filters, only: [:index, :create, :show, :update, :destroy] do - resources :keywords, only: [:index, :create], controller: 'filters/keywords' - resources :statuses, only: [:index, :create], controller: 'filters/statuses' - end - - namespace :filters do - resources :keywords, only: [:show, :update, :destroy] - resources :statuses, only: [:show, :destroy] - end - - namespace :admin do - resources :accounts, only: [:index] - end - end - - namespace :web do - resource :settings, only: [:update] - resource :embed, only: [:create] - resources :push_subscriptions, only: [:create] do - member do - put :update - end - end - end - end + draw(:api) web_app_paths.each do |path| get path, to: 'home#index' diff --git a/config/routes/admin.rb b/config/routes/admin.rb new file mode 100644 index 0000000000..850994b6d0 --- /dev/null +++ b/config/routes/admin.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +namespace :admin do + get '/dashboard', to: 'dashboard#index' + + resources :domain_allows, only: [:new, :create, :show, :destroy] + resources :domain_blocks, only: [:new, :create, :destroy, :update, :edit] do + collection do + post :batch + end + end + + resources :export_domain_allows, only: [:new] do + collection do + get :export, constraints: { format: :csv } + post :import + end + end + + resources :export_domain_blocks, only: [:new] do + collection do + get :export, constraints: { format: :csv } + post :import + end + end + + resources :email_domain_blocks, only: [:index, :new, :create] do + collection do + post :batch + end + end + + resources :action_logs, only: [:index] + resources :warning_presets, except: [:new] + + resources :announcements, except: [:show] do + member do + post :publish + post :unpublish + end + end + + get '/settings', to: redirect('/admin/settings/branding') + get '/settings/edit', to: redirect('/admin/settings/branding') + + namespace :settings do + resource :branding, only: [:show, :update], controller: 'branding' + resource :registrations, only: [:show, :update], controller: 'registrations' + resource :content_retention, only: [:show, :update], controller: 'content_retention' + resource :about, only: [:show, :update], controller: 'about' + resource :appearance, only: [:show, :update], controller: 'appearance' + resource :discovery, only: [:show, :update], controller: 'discovery' + resource :other, only: [:show, :update], controller: 'other' + end + + resources :site_uploads, only: [:destroy] + + resources :invites, only: [:index, :create, :destroy] do + collection do + post :deactivate_all + end + end + + resources :relays, only: [:index, :new, :create, :destroy] do + member do + post :enable + post :disable + end + end + + resources :instances, only: [:index, :show, :destroy], constraints: { id: %r{[^/]+} } do + member do + post :clear_delivery_errors + post :restart_delivery + post :stop_delivery + end + end + + resources :rules + + resources :webhooks do + member do + post :enable + post :disable + end + + resource :secret, only: [], controller: 'webhooks/secrets' do + post :rotate + end + end + + resources :reports, only: [:index, :show] do + resources :actions, only: [:create], controller: 'reports/actions' do + collection do + post :preview + end + end + + member do + post :assign_to_self + post :unassign + post :reopen + post :resolve + end + end + + resources :report_notes, only: [:create, :destroy] + + resources :accounts, only: [:index, :show, :destroy] do + member do + post :enable + post :unsensitive + post :unsilence + post :unsuspend + post :redownload + post :remove_avatar + post :remove_header + post :memorialize + post :approve + post :reject + post :unblock_email + end + + collection do + post :batch + end + + resource :change_email, only: [:show, :update] + resource :reset, only: [:create] + resource :action, only: [:new, :create], controller: 'account_actions' + + resources :statuses, only: [:index, :show] do + collection do + post :batch + end + end + + resources :relationships, only: [:index] + + resource :confirmation, only: [:create] do + collection do + post :resend + end + end + end + + resources :users, only: [] do + resource :two_factor_authentication, only: [:destroy], controller: 'users/two_factor_authentications' + resource :role, only: [:show, :update], controller: 'users/roles' + end + + resources :custom_emojis, only: [:index, :new, :create] do + collection do + post :batch + end + end + + resources :ip_blocks, only: [:index, :new, :create] do + collection do + post :batch + end + end + + resources :roles, except: [:show] + resources :account_moderation_notes, only: [:create, :destroy] + resource :follow_recommendations, only: [:show, :update] + resources :tags, only: [:show, :update] + + namespace :trends do + resources :links, only: [:index] do + collection do + post :batch + end + end + + resources :tags, only: [:index] do + collection do + post :batch + end + end + + resources :statuses, only: [:index] do + collection do + post :batch + end + end + + namespace :links do + resources :preview_card_providers, only: [:index], path: :publishers do + collection do + post :batch + end + end + end + end + + namespace :disputes do + resources :appeals, only: [:index] do + member do + post :approve + post :reject + end + end + end +end diff --git a/config/routes/api.rb b/config/routes/api.rb new file mode 100644 index 0000000000..1ca4d41beb --- /dev/null +++ b/config/routes/api.rb @@ -0,0 +1,306 @@ +# frozen_string_literal: true + +namespace :api, format: false do + # OEmbed + get '/oembed', to: 'oembed#show', as: :oembed + + # JSON / REST API + namespace :v1 do + resources :statuses, only: [:create, :show, :update, :destroy] do + scope module: :statuses do + resources :reblogged_by, controller: :reblogged_by_accounts, only: :index + resources :favourited_by, controller: :favourited_by_accounts, only: :index + resource :reblog, only: :create + post :unreblog, to: 'reblogs#destroy' + + resource :favourite, only: :create + post :unfavourite, to: 'favourites#destroy' + + resource :bookmark, only: :create + post :unbookmark, to: 'bookmarks#destroy' + + resource :mute, only: :create + post :unmute, to: 'mutes#destroy' + + resource :pin, only: :create + post :unpin, to: 'pins#destroy' + + resource :history, only: :show + resource :source, only: :show + + post :translate, to: 'translations#create' + end + + member do + get :context + end + end + + namespace :timelines do + resource :direct, only: :show, controller: :direct + resource :home, only: :show, controller: :home + resource :public, only: :show, controller: :public + resources :tag, only: :show + resources :list, only: :show + end + + get '/streaming', to: 'streaming#index' + get '/streaming/(*any)', to: 'streaming#index' + + resources :custom_emojis, only: [:index] + resources :suggestions, only: [:index, :destroy] + resources :scheduled_statuses, only: [:index, :show, :update, :destroy] + resources :preferences, only: [:index] + + resources :announcements, only: [:index] do + scope module: :announcements do + resources :reactions, only: [:update, :destroy] + end + + member do + post :dismiss + end + end + + # namespace :crypto do + # resources :deliveries, only: :create + + # namespace :keys do + # resource :upload, only: [:create] + # resource :query, only: [:create] + # resource :claim, only: [:create] + # resource :count, only: [:show] + # end + + # resources :encrypted_messages, only: [:index] do + # collection do + # post :clear + # end + # end + # end + + resources :conversations, only: [:index, :destroy] do + member do + post :read + end + end + + resources :media, only: [:create, :update, :show] + resources :blocks, only: [:index] + resources :mutes, only: [:index] + resources :favourites, only: [:index] + resources :bookmarks, only: [:index] + resources :reports, only: [:create] + resources :trends, only: [:index], controller: 'trends/tags' + resources :filters, only: [:index, :create, :show, :update, :destroy] + resources :endorsements, only: [:index] + resources :markers, only: [:index, :create] + + namespace :apps do + get :verify_credentials, to: 'credentials#show' + end + + resources :apps, only: [:create] + + namespace :trends do + resources :tags, only: [:index] + resources :links, only: [:index] + resources :statuses, only: [:index] + end + + namespace :emails do + resources :confirmations, only: [:create] + end + + resource :instance, only: [:show] do + resources :peers, only: [:index], controller: 'instances/peers' + resources :rules, only: [:index], controller: 'instances/rules' + resources :domain_blocks, only: [:index], controller: 'instances/domain_blocks' + resource :privacy_policy, only: [:show], controller: 'instances/privacy_policies' + resource :extended_description, only: [:show], controller: 'instances/extended_descriptions' + resource :translation_languages, only: [:show], controller: 'instances/translation_languages' + resource :activity, only: [:show], controller: 'instances/activity' + end + + resource :domain_blocks, only: [:show, :create, :destroy] + + resource :directory, only: [:show] + + resources :follow_requests, only: [:index] do + member do + post :authorize + post :reject + end + end + + resources :notifications, only: [:index, :show, :destroy] do + collection do + post :clear + delete :destroy_multiple + end + + member do + post :dismiss + end + end + + namespace :accounts do + get :verify_credentials, to: 'credentials#show' + patch :update_credentials, to: 'credentials#update' + resource :search, only: :show, controller: :search + resource :lookup, only: :show, controller: :lookup + resources :relationships, only: :index + resources :familiar_followers, only: :index + end + + resources :accounts, only: [:create, :show] do + resources :statuses, only: :index, controller: 'accounts/statuses' + resources :followers, only: :index, controller: 'accounts/follower_accounts' + resources :following, only: :index, controller: 'accounts/following_accounts' + resources :lists, only: :index, controller: 'accounts/lists' + resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs' + resources :featured_tags, only: :index, controller: 'accounts/featured_tags' + + member do + post :follow + post :unfollow + post :remove_from_followers + post :block + post :unblock + post :mute + post :unmute + end + + resource :pin, only: :create, controller: 'accounts/pins' + post :unpin, to: 'accounts/pins#destroy' + resource :note, only: :create, controller: 'accounts/notes' + end + + resources :tags, only: [:show] do + member do + post :follow + post :unfollow + end + end + + resources :followed_tags, only: [:index] + + resources :lists, only: [:index, :create, :show, :update, :destroy] do + resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts' + end + + namespace :featured_tags do + get :suggestions, to: 'suggestions#index' + end + + resources :featured_tags, only: [:index, :create, :destroy] + + resources :polls, only: [:create, :show] do + resources :votes, only: :create, controller: 'polls/votes' + end + + namespace :push do + resource :subscription, only: [:create, :show, :update, :destroy] + end + + namespace :admin do + resources :accounts, only: [:index, :show, :destroy] do + member do + post :enable + post :unsensitive + post :unsilence + post :unsuspend + post :approve + post :reject + end + + resource :action, only: [:create], controller: 'account_actions' + end + + resources :reports, only: [:index, :update, :show] do + member do + post :assign_to_self + post :unassign + post :reopen + post :resolve + end + end + + resources :domain_allows, only: [:index, :show, :create, :destroy] + resources :domain_blocks, only: [:index, :show, :update, :create, :destroy] + resources :email_domain_blocks, only: [:index, :show, :create, :destroy] + resources :ip_blocks, only: [:index, :show, :update, :create, :destroy] + + namespace :trends do + resources :tags, only: [:index] do + member do + post :approve + post :reject + end + end + resources :links, only: [:index] do + member do + post :approve + post :reject + end + end + resources :statuses, only: [:index] do + member do + post :approve + post :reject + end + end + + namespace :links do + resources :preview_card_providers, only: [:index], path: :publishers do + member do + post :approve + post :reject + end + end + end + end + + post :measures, to: 'measures#create' + post :dimensions, to: 'dimensions#create' + post :retention, to: 'retention#create' + + resources :canonical_email_blocks, only: [:index, :create, :show, :destroy] do + collection do + post :test + end + end + end + end + + namespace :v2 do + get '/search', to: 'search#index', as: :search + + resources :media, only: [:create] + resources :suggestions, only: [:index] + resource :instance, only: [:show] + resources :filters, only: [:index, :create, :show, :update, :destroy] do + resources :keywords, only: [:index, :create], controller: 'filters/keywords' + resources :statuses, only: [:index, :create], controller: 'filters/statuses' + end + + namespace :filters do + resources :keywords, only: [:show, :update, :destroy] + resources :statuses, only: [:show, :destroy] + end + + namespace :admin do + resources :accounts, only: [:index] + end + end + + namespace :web do + resource :settings, only: [:update] + resource :embed, only: [:create] + resources :push_subscriptions, only: [:create] do + member do + put :update + end + end + end +end diff --git a/config/routes/settings.rb b/config/routes/settings.rb new file mode 100644 index 0000000000..ad1f7fce64 --- /dev/null +++ b/config/routes/settings.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +namespace :settings do + resource :profile, only: [:show, :update] do + resources :pictures, only: :destroy + end + + get :preferences, to: redirect('/settings/preferences/appearance') + + namespace :preferences do + resource :appearance, only: [:show, :update], controller: :appearance + resource :notifications, only: [:show, :update] + resource :other, only: [:show, :update], controller: :other + end + + resources :imports, only: [:index, :show, :destroy, :create] do + member do + post :confirm + get :failures + end + end + + resource :export, only: [:show, :create] + + namespace :exports, constraints: { format: :csv } do + resources :follows, only: :index, controller: :following_accounts + resources :blocks, only: :index, controller: :blocked_accounts + resources :mutes, only: :index, controller: :muted_accounts + resources :lists, only: :index, controller: :lists + resources :domain_blocks, only: :index, controller: :blocked_domains + resources :bookmarks, only: :index, controller: :bookmarks + end + + resources :two_factor_authentication_methods, only: [:index] do + collection do + post :disable + end + end + + resource :otp_authentication, only: [:show, :create], controller: 'two_factor_authentication/otp_authentication' + + resources :webauthn_credentials, only: [:index, :new, :create, :destroy], + path: 'security_keys', + controller: 'two_factor_authentication/webauthn_credentials' do + collection do + get :options + end + end + + namespace :two_factor_authentication do + resources :recovery_codes, only: [:create] + resource :confirmation, only: [:new, :create] + end + + resources :applications, except: [:edit] do + member do + post :regenerate + end + end + + resources :flavours, only: [:index, :show, :update], param: :flavour + + resource :delete, only: [:show, :destroy] + resource :migration, only: [:show, :create] + + namespace :migration do + resource :redirect, only: [:new, :create, :destroy] + end + + resources :aliases, only: [:index, :create, :destroy] + resources :sessions, only: [:destroy] + resources :featured_tags, only: [:index, :create, :destroy] + resources :login_activities, only: [:index] +end diff --git a/db/migrate/20230330135507_create_bulk_imports.rb b/db/migrate/20230330135507_create_bulk_imports.rb new file mode 100644 index 0000000000..117a135086 --- /dev/null +++ b/db/migrate/20230330135507_create_bulk_imports.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class CreateBulkImports < ActiveRecord::Migration[6.1] + def change + create_table :bulk_imports do |t| + t.integer :type, null: false + t.integer :state, null: false + t.integer :total_items, null: false, default: 0 + t.integer :imported_items, null: false, default: 0 + t.integer :processed_items, null: false, default: 0 + t.datetime :finished_at + t.boolean :overwrite, null: false, default: false + t.boolean :likely_mismatched, null: false, default: false + t.string :original_filename, null: false, default: '' + t.references :account, null: false, foreign_key: { on_delete: :cascade } + + t.timestamps + end + + add_index :bulk_imports, [:id], name: :index_bulk_imports_unconfirmed, where: 'state = 0' + end +end diff --git a/db/migrate/20230330140036_create_bulk_import_rows.rb b/db/migrate/20230330140036_create_bulk_import_rows.rb new file mode 100644 index 0000000000..ef6ad7069b --- /dev/null +++ b/db/migrate/20230330140036_create_bulk_import_rows.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CreateBulkImportRows < ActiveRecord::Migration[6.1] + def change + create_table :bulk_import_rows do |t| + t.references :bulk_import, null: false, foreign_key: { on_delete: :cascade } + t.jsonb :data + + t.timestamps + end + end +end diff --git a/db/migrate/20230330155710_add_follow_request_id_to_list_accounts.rb b/db/migrate/20230330155710_add_follow_request_id_to_list_accounts.rb new file mode 100644 index 0000000000..5f4ac374ca --- /dev/null +++ b/db/migrate/20230330155710_add_follow_request_id_to_list_accounts.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddFollowRequestIdToListAccounts < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + safety_assured { add_reference :list_accounts, :follow_request, foreign_key: { on_delete: :cascade }, index: false } + add_index :list_accounts, :follow_request_id, algorithm: :concurrently, where: 'follow_request_id IS NOT NULL' + end +end diff --git a/db/schema.rb b/db/schema.rb index 7d894b1aa1..e834d7e09b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_02_15_074424) do +ActiveRecord::Schema.define(version: 2023_03_30_155710) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -294,6 +294,31 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do t.index ["status_id"], name: "index_bookmarks_on_status_id" end + create_table "bulk_import_rows", force: :cascade do |t| + t.bigint "bulk_import_id", null: false + t.jsonb "data" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["bulk_import_id"], name: "index_bulk_import_rows_on_bulk_import_id" + end + + create_table "bulk_imports", force: :cascade do |t| + t.integer "type", null: false + t.integer "state", null: false + t.integer "total_items", default: 0, null: false + t.integer "imported_items", default: 0, null: false + t.integer "processed_items", default: 0, null: false + t.datetime "finished_at" + t.boolean "overwrite", default: false, null: false + t.boolean "likely_mismatched", default: false, null: false + t.string "original_filename", default: "", null: false + t.bigint "account_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["account_id"], name: "index_bulk_imports_on_account_id" + t.index ["id"], name: "index_bulk_imports_unconfirmed", where: "(state = 0)" + end + create_table "canonical_email_blocks", force: :cascade do |t| t.string "canonical_email_hash", default: "", null: false t.bigint "reference_account_id" @@ -529,8 +554,10 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do t.bigint "list_id", null: false t.bigint "account_id", null: false t.bigint "follow_id" + t.bigint "follow_request_id" t.index ["account_id", "list_id"], name: "index_list_accounts_on_account_id_and_list_id", unique: true t.index ["follow_id"], name: "index_list_accounts_on_follow_id", where: "(follow_id IS NOT NULL)" + t.index ["follow_request_id"], name: "index_list_accounts_on_follow_request_id", where: "(follow_request_id IS NOT NULL)" t.index ["list_id", "account_id"], name: "index_list_accounts_on_list_id_and_account_id" end @@ -1149,6 +1176,8 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade add_foreign_key "bookmarks", "accounts", on_delete: :cascade add_foreign_key "bookmarks", "statuses", on_delete: :cascade + add_foreign_key "bulk_import_rows", "bulk_imports", on_delete: :cascade + add_foreign_key "bulk_imports", "accounts", on_delete: :cascade add_foreign_key "canonical_email_blocks", "accounts", column: "reference_account_id", on_delete: :cascade add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade @@ -1174,6 +1203,7 @@ ActiveRecord::Schema.define(version: 2023_02_15_074424) do add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade add_foreign_key "invites", "users", on_delete: :cascade add_foreign_key "list_accounts", "accounts", on_delete: :cascade + add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade add_foreign_key "list_accounts", "follows", on_delete: :cascade add_foreign_key "list_accounts", "lists", on_delete: :cascade add_foreign_key "lists", "accounts", on_delete: :cascade diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index 43269334a5..c09577c9ef 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -121,10 +121,10 @@ module Mastodon say('OK', :green) say("New password: #{password}") else - user.errors.to_h.each do |key, error| + user.errors.each do |error| say('Failure/Error: ', :red) - say(key) - say(" #{error}", :red) + say(error.attribute) + say(" #{error.type}", :red) end exit(1) @@ -197,10 +197,10 @@ module Mastodon say('OK', :green) say("New password: #{password}") if options[:reset_password] else - user.errors.to_h.each do |key, error| + user.errors.each do |error| say('Failure/Error: ', :red) - say(key) - say(" #{error}", :red) + say(error.attribute) + say(" #{error.type}", :red) end exit(1) @@ -353,7 +353,7 @@ module Mastodon begin code = Request.new(:head, account.uri).perform(&:code) - rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError + rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Mastodon::PrivateNetworkAddressError skip_domains << account.domain end diff --git a/lib/mastodon/emoji_cli.rb b/lib/mastodon/emoji_cli.rb index 88065c2a39..8f2432a3e2 100644 --- a/lib/mastodon/emoji_cli.rb +++ b/lib/mastodon/emoji_cli.rb @@ -73,7 +73,6 @@ module Mastodon end end - puts say("Imported #{imported}, skipped #{skipped}, failed to import #{failed}", color(imported, skipped, failed)) end diff --git a/lib/mastodon/maintenance_cli.rb b/lib/mastodon/maintenance_cli.rb index ff8f6ddda9..88bd191ea9 100644 --- a/lib/mastodon/maintenance_cli.rb +++ b/lib/mastodon/maintenance_cli.rb @@ -664,9 +664,7 @@ module Mastodon def remove_index_if_exists!(table, name) ActiveRecord::Base.connection.remove_index(table, name: name) - rescue ArgumentError - nil - rescue ActiveRecord::StatementInvalid + rescue ArgumentError, ActiveRecord::StatementInvalid nil end end diff --git a/lib/mastodon/media_cli.rb b/lib/mastodon/media_cli.rb index b2dfe58d53..7dacd8d3d4 100644 --- a/lib/mastodon/media_cli.rb +++ b/lib/mastodon/media_cli.rb @@ -9,6 +9,8 @@ module Mastodon include ActionView::Helpers::NumberHelper include CLIHelper + VALID_PATH_SEGMENTS_SIZE = [7, 10].freeze + def self.exit_on_failure? true end @@ -133,7 +135,7 @@ module Mastodon path_segments = object.key.split('/') path_segments.delete('cache') - unless [7, 10].include?(path_segments.size) + unless VALID_PATH_SEGMENTS_SIZE.include?(path_segments.size) progress.log(pastel.yellow("Unrecognized file found: #{object.key}")) next end @@ -177,7 +179,7 @@ module Mastodon path_segments = key.split(File::SEPARATOR) path_segments.delete('cache') - unless [7, 10].include?(path_segments.size) + unless VALID_PATH_SEGMENTS_SIZE.include?(path_segments.size) progress.log(pastel.yellow("Unrecognized file found: #{key}")) next end @@ -310,7 +312,7 @@ module Mastodon path_segments = path.split('/')[2..] path_segments.delete('cache') - unless [7, 10].include?(path_segments.size) + unless VALID_PATH_SEGMENTS_SIZE.include?(path_segments.size) say('Not a media URL', :red) exit(1) end @@ -363,7 +365,7 @@ module Mastodon segments = object.key.split('/') segments.delete('cache') - next unless [7, 10].include?(segments.size) + next unless VALID_PATH_SEGMENTS_SIZE.include?(segments.size) model_name = segments.first.classify record_id = segments[2..-2].join.to_i diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 408f60185b..f71762d993 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -17,11 +17,11 @@ module Mastodon end def flags - '' + ENV.fetch('MASTODON_VERSION_FLAGS', '') end def suffix - '+glitch' + "+glitch#{ENV.fetch('MASTODON_VERSION_SUFFIX', '')}" end def to_a diff --git a/lib/paperclip/color_extractor.rb b/lib/paperclip/color_extractor.rb index 2e8dc04fd8..19529fb8a1 100644 --- a/lib/paperclip/color_extractor.rb +++ b/lib/paperclip/color_extractor.rb @@ -173,7 +173,7 @@ module Paperclip def palette_from_histogram(result, quantity) frequencies = result.scan(/([0-9]+)\:/).flatten.map(&:to_f) hex_values = result.scan(/\#([0-9A-Fa-f]{6,8})/).flatten - total_frequencies = frequencies.reduce(&:+).to_f + total_frequencies = frequencies.sum.to_f frequencies.map.with_index { |f, i| [f / total_frequencies, hex_values[i]] } .sort_by { |r| -r[0] } diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb index 4c0e9b8580..0ee3a519b6 100644 --- a/lib/sanitize_ext/sanitize_config.rb +++ b/lib/sanitize_ext/sanitize_config.rb @@ -127,7 +127,7 @@ class Sanitize node = env[:node] rel = (node['rel'] || '').split & ['tag'] - rel += ['nofollow', 'noopener', 'noreferrer'] unless TagManager.instance.local_url?(node['href']) + rel += %w(nofollow noopener noreferrer) unless TagManager.instance.local_url?(node['href']) if rel.empty? node.remove_attribute('rel') diff --git a/lib/terrapin/multi_pipe_extensions.rb b/lib/terrapin/multi_pipe_extensions.rb index 209f4ad6ce..bddba4a2b1 100644 --- a/lib/terrapin/multi_pipe_extensions.rb +++ b/lib/terrapin/multi_pipe_extensions.rb @@ -13,7 +13,7 @@ module Terrapin def pipe_options # Add some flags to explicitly close the other end of the pipes - { out: @stdout_out, err: @stderr_out, @stdout_in => :close, @stderr_in => :close } + { :out => @stdout_out, :err => @stderr_out, @stdout_in => :close, @stderr_in => :close } end def read diff --git a/package.json b/package.json index 32c8a3f8d6..f5db9e94f2 100644 --- a/package.json +++ b/package.json @@ -26,22 +26,23 @@ }, "private": true, "dependencies": { - "@babel/core": "^7.21.4", + "@babel/core": "^7.21.8", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", "@babel/plugin-transform-react-inline-elements": "^7.21.0", "@babel/plugin-transform-runtime": "^7.21.4", - "@babel/preset-env": "^7.21.4", + "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", - "@babel/preset-typescript": "^7.21.4", - "@babel/runtime": "^7.21.0", + "@babel/preset-typescript": "^7.21.5", + "@babel/runtime": "^7.21.5", "@gamestdio/websocket": "^0.3.2", "@github/webauthn-json": "^2.1.1", "@rails/ujs": "^6.1.7", + "@reduxjs/toolkit": "^1.9.5", "abortcontroller-polyfill": "^1.7.5", "atrament": "0.2.4", "arrow-key-navigation": "^1.2.0", "autoprefixer": "^10.4.14", - "axios": "^1.3.6", + "axios": "^1.4.0", "babel-loader": "^8.3.0", "babel-plugin-lodash": "^3.3.4", "babel-plugin-preval": "^5.1.0", @@ -54,7 +55,7 @@ "compression-webpack-plugin": "^6.1.1", "cross-env": "^7.0.3", "css-loader": "^5.2.7", - "cssnano": "^6.0.0", + "cssnano": "^6.0.1", "detect-passive-events": "^2.0.3", "dotenv": "^16.0.3", "emoji-mart": "npm:emoji-mart-lazyload@latest", @@ -75,7 +76,7 @@ "intl-messageformat": "^2.2.0", "intl-relativeformat": "^6.4.3", "js-yaml": "^4.1.0", - "jsdom": "^21.1.1", + "jsdom": "^21.1.2", "lodash": "^4.17.21", "mark-loader": "^0.1.6", "mini-css-extract-plugin": "^1.6.2", @@ -105,7 +106,7 @@ "react-redux-loading-bar": "^5.0.4", "react-router-dom": "^4.1.1", "react-router-scroll-4": "^1.0.0-beta.1", - "react-select": "^5.7.2", + "react-select": "^5.7.3", "react-sparklines": "^1.7.0", "react-swipeable-views": "^0.14.0", "react-textarea-autosize": "^8.4.1", @@ -180,11 +181,11 @@ "@types/uuid": "^8.3.4", "@types/webpack": "^4.41.33", "@types/yargs": "^17.0.24", - "@typescript-eslint/eslint-plugin": "^5.59.1", - "@typescript-eslint/parser": "^5.59.1", + "@typescript-eslint/eslint-plugin": "^5.59.2", + "@typescript-eslint/parser": "^5.59.2", "babel-jest": "^29.5.0", "eslint": "^8.39.0", - "eslint-plugin-formatjs": "^4.9.0", + "eslint-plugin-formatjs": "^4.10.1", "eslint-plugin-import": "~2.27.5", "eslint-plugin-jsdoc": "^43.1.1", "eslint-plugin-jsx-a11y": "~6.7.1", @@ -199,11 +200,11 @@ "raf": "^3.4.1", "react-intl-translations-manager": "^5.0.3", "react-test-renderer": "^16.14.0", - "stylelint": "^15.6.0", + "stylelint": "^15.6.1", "stylelint-config-standard-scss": "^9.0.0", "typescript": "^5.0.4", "webpack-dev-server": "^3.11.3", - "yargs": "^17.7.1" + "yargs": "^17.7.2" }, "resolutions": { "kind-of": "^6.0.3", diff --git a/spec/config/initializers/rack_attack_spec.rb b/spec/config/initializers/rack_attack_spec.rb index 0411a48d2a..7cd4ac76bb 100644 --- a/spec/config/initializers/rack_attack_spec.rb +++ b/spec/config/initializers/rack_attack_spec.rb @@ -46,36 +46,36 @@ describe Rack::Attack, type: :request do let(:remote_ip) { '1.2.3.5' } describe 'throttle excessive sign-up requests by IP address' do - context 'through the website' do + context 'when accessed through the website' do let(:limit) { 25 } let(:period) { 5.minutes } let(:request) { -> { post path, headers: { 'REMOTE_ADDR' => remote_ip } } } - context 'for exact path' do + context 'with exact path' do let(:path) { '/auth' } it_behaves_like 'throttled endpoint' end - context 'for path with format' do + context 'with path with format' do let(:path) { '/auth.html' } it_behaves_like 'throttled endpoint' end end - context 'through the API' do + context 'when accessed through the API' do let(:limit) { 5 } let(:period) { 30.minutes } let(:request) { -> { post path, headers: { 'REMOTE_ADDR' => remote_ip } } } - context 'for exact path' do + context 'with exact path' do let(:path) { '/api/v1/accounts' } it_behaves_like 'throttled endpoint' end - context 'for path with format' do + context 'with path with format' do let(:path) { '/api/v1/accounts.json' } it 'returns http not found' do @@ -91,13 +91,13 @@ describe Rack::Attack, type: :request do let(:period) { 5.minutes } let(:request) { -> { post path, headers: { 'REMOTE_ADDR' => remote_ip } } } - context 'for exact path' do + context 'with exact path' do let(:path) { '/auth/sign_in' } it_behaves_like 'throttled endpoint' end - context 'for path with format' do + context 'with path with format' do let(:path) { '/auth/sign_in.html' } it_behaves_like 'throttled endpoint' diff --git a/spec/controllers/about_controller_spec.rb b/spec/controllers/about_controller_spec.rb index ccd28a96ce..8db6d80b0b 100644 --- a/spec/controllers/about_controller_spec.rb +++ b/spec/controllers/about_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe AboutController, type: :controller do +RSpec.describe AboutController do render_views describe 'GET #show' do diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 53fc75659a..3667564802 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe AccountsController, type: :controller do +RSpec.describe AccountsController do render_views let(:account) { Fabricate(:account) } @@ -57,7 +57,7 @@ RSpec.describe AccountsController, type: :controller do end end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it_behaves_like 'preliminary checks' @@ -140,7 +140,7 @@ RSpec.describe AccountsController, type: :controller do end end - context 'as JSON' do + context 'with JSON' do let(:authorized_fetch_mode) { false } let(:format) { 'json' } @@ -193,7 +193,7 @@ RSpec.describe AccountsController, type: :controller do expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) end - context 'in authorized fetch mode' do + context 'with authorized fetch mode' do let(:authorized_fetch_mode) { true } it 'returns http unauthorized' do @@ -251,7 +251,7 @@ RSpec.describe AccountsController, type: :controller do expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) end - context 'in authorized fetch mode' do + context 'with authorized fetch mode' do let(:authorized_fetch_mode) { true } it 'returns http success' do @@ -278,7 +278,7 @@ RSpec.describe AccountsController, type: :controller do end end - context 'as RSS' do + context 'with RSS' do let(:format) { 'rss' } it_behaves_like 'preliminary checks' diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/controllers/activitypub/collections_controller_spec.rb index 77901131e7..8878474885 100644 --- a/spec/controllers/activitypub/collections_controller_spec.rb +++ b/spec/controllers/activitypub/collections_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ActivityPub::CollectionsController, type: :controller do +RSpec.describe ActivityPub::CollectionsController do let!(:account) { Fabricate(:account) } let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) } let(:remote_account) { nil } @@ -35,10 +35,9 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do describe 'GET #show' do context 'when id is "featured"' do context 'without signature' do - subject(:body) { body_as_json } - subject(:response) { get :show, params: { id: 'featured', account_username: account.username } } + let(:body) { body_as_json } let(:remote_account) { nil } it 'returns http success' do @@ -120,7 +119,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do end end - context 'in authorized fetch mode' do + context 'with authorized fetch mode' do before do allow(controller).to receive(:authorized_fetch_mode?).and_return(true) end diff --git a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb index c99d59edaa..8fcce165b3 100644 --- a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb +++ b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controller do +RSpec.describe ActivityPub::FollowersSynchronizationsController do let!(:account) { Fabricate(:account) } let!(:follower_1) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/a') } let!(:follower_2) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/b') } @@ -34,10 +34,9 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controll end context 'with signature from example.com' do - subject(:body) { body_as_json } - subject(:response) { get :show, params: { account_username: account.username } } + let(:body) { body_as_json } let(:remote_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/instance') } it 'returns http success' do diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb index 8d4084648d..030a303266 100644 --- a/spec/controllers/activitypub/inboxes_controller_spec.rb +++ b/spec/controllers/activitypub/inboxes_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ActivityPub::InboxesController, type: :controller do +RSpec.describe ActivityPub::InboxesController do let(:remote_account) { nil } before do @@ -21,7 +21,7 @@ RSpec.describe ActivityPub::InboxesController, type: :controller do expect(response).to have_http_status(202) end - context 'for a specific account' do + context 'with a specific account' do subject(:response) { post :create, params: { account_username: account.username }, body: '{}' } let(:account) { Fabricate(:account) } diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb index 167bbcc21d..8823d9fe7e 100644 --- a/spec/controllers/activitypub/outboxes_controller_spec.rb +++ b/spec/controllers/activitypub/outboxes_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ActivityPub::OutboxesController, type: :controller do +RSpec.describe ActivityPub::OutboxesController do let!(:account) { Fabricate(:account) } shared_examples 'cacheable response' do @@ -35,10 +35,9 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do describe 'GET #show' do context 'without signature' do - subject(:body) { body_as_json } - subject(:response) { get :show, params: { account_username: account.username, page: page } } + let(:body) { body_as_json } let(:remote_account) { nil } context 'with page not requested' do diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/controllers/activitypub/replies_controller_spec.rb index 582ef863f2..c7b65f004d 100644 --- a/spec/controllers/activitypub/replies_controller_spec.rb +++ b/spec/controllers/activitypub/replies_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ActivityPub::RepliesController, type: :controller do +RSpec.describe ActivityPub::RepliesController do let(:status) { Fabricate(:status, visibility: parent_visibility) } let(:remote_account) { Fabricate(:account, domain: 'foobar.com') } let(:remote_reply_id) { 'https://foobar.com/statuses/1234' } diff --git a/spec/controllers/admin/account_moderation_notes_controller_spec.rb b/spec/controllers/admin/account_moderation_notes_controller_spec.rb index d2c52f5940..848281c290 100644 --- a/spec/controllers/admin/account_moderation_notes_controller_spec.rb +++ b/spec/controllers/admin/account_moderation_notes_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::AccountModerationNotesController, type: :controller do +RSpec.describe Admin::AccountModerationNotesController do render_views let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb index b182715b0b..7d001c4cbc 100644 --- a/spec/controllers/admin/accounts_controller_spec.rb +++ b/spec/controllers/admin/accounts_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::AccountsController, type: :controller do +RSpec.describe Admin::AccountsController do render_views before { sign_in current_user, scope: :user } diff --git a/spec/controllers/admin/action_logs_controller_spec.rb b/spec/controllers/admin/action_logs_controller_spec.rb index 7cd8cdf462..044ddf2c42 100644 --- a/spec/controllers/admin/action_logs_controller_spec.rb +++ b/spec/controllers/admin/action_logs_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe Admin::ActionLogsController, type: :controller do +describe Admin::ActionLogsController do render_views # Action logs typically cause issues when their targets are not in the database diff --git a/spec/controllers/admin/base_controller_spec.rb b/spec/controllers/admin/base_controller_spec.rb index bfb9d2c7d4..1f1fa8441a 100644 --- a/spec/controllers/admin/base_controller_spec.rb +++ b/spec/controllers/admin/base_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe Admin::BaseController, type: :controller do +describe Admin::BaseController do controller do def success authorize :dashboard, :index? diff --git a/spec/controllers/admin/change_emails_controller_spec.rb b/spec/controllers/admin/change_emails_controller_spec.rb index 8329984715..503862a7b9 100644 --- a/spec/controllers/admin/change_emails_controller_spec.rb +++ b/spec/controllers/admin/change_emails_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::ChangeEmailsController, type: :controller do +RSpec.describe Admin::ChangeEmailsController do render_views let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } diff --git a/spec/controllers/admin/confirmations_controller_spec.rb b/spec/controllers/admin/confirmations_controller_spec.rb index d05711e272..ffab56d9aa 100644 --- a/spec/controllers/admin/confirmations_controller_spec.rb +++ b/spec/controllers/admin/confirmations_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::ConfirmationsController, type: :controller do +RSpec.describe Admin::ConfirmationsController do render_views before do diff --git a/spec/controllers/admin/dashboard_controller_spec.rb b/spec/controllers/admin/dashboard_controller_spec.rb index ab3738fcd7..25300fdd90 100644 --- a/spec/controllers/admin/dashboard_controller_spec.rb +++ b/spec/controllers/admin/dashboard_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe Admin::DashboardController, type: :controller do +describe Admin::DashboardController do render_views describe 'GET #index' do diff --git a/spec/controllers/admin/disputes/appeals_controller_spec.rb b/spec/controllers/admin/disputes/appeals_controller_spec.rb index 576a0c12b9..371c4f483d 100644 --- a/spec/controllers/admin/disputes/appeals_controller_spec.rb +++ b/spec/controllers/admin/disputes/appeals_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::Disputes::AppealsController, type: :controller do +RSpec.describe Admin::Disputes::AppealsController do render_views before { sign_in current_user, scope: :user } diff --git a/spec/controllers/admin/domain_allows_controller_spec.rb b/spec/controllers/admin/domain_allows_controller_spec.rb index 2a0f47145a..6b0453476a 100644 --- a/spec/controllers/admin/domain_allows_controller_spec.rb +++ b/spec/controllers/admin/domain_allows_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::DomainAllowsController, type: :controller do +RSpec.describe Admin::DomainAllowsController do render_views before do diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb index ef13f76762..6aed172ac5 100644 --- a/spec/controllers/admin/domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/domain_blocks_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::DomainBlocksController, type: :controller do +RSpec.describe Admin::DomainBlocksController do render_views before do @@ -83,7 +83,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do BlockDomainService.new.call(domain_block) end - context 'downgrading a domain suspension to silence' do + context 'when downgrading a domain suspension to silence' do let(:original_severity) { 'suspend' } let(:new_severity) { 'silence' } @@ -100,7 +100,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do end end - context 'upgrading a domain silence to suspend' do + context 'when upgrading a domain silence to suspend' do let(:original_severity) { 'silence' } let(:new_severity) { 'suspend' } diff --git a/spec/controllers/admin/email_domain_blocks_controller_spec.rb b/spec/controllers/admin/email_domain_blocks_controller_spec.rb index e9cef4a94c..4286600144 100644 --- a/spec/controllers/admin/email_domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/email_domain_blocks_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::EmailDomainBlocksController, type: :controller do +RSpec.describe Admin::EmailDomainBlocksController do render_views before do diff --git a/spec/controllers/admin/export_domain_allows_controller_spec.rb b/spec/controllers/admin/export_domain_allows_controller_spec.rb index f12bd1344f..9d50c04aad 100644 --- a/spec/controllers/admin/export_domain_allows_controller_spec.rb +++ b/spec/controllers/admin/export_domain_allows_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::ExportDomainAllowsController, type: :controller do +RSpec.describe Admin::ExportDomainAllowsController do render_views before do diff --git a/spec/controllers/admin/export_domain_blocks_controller_spec.rb b/spec/controllers/admin/export_domain_blocks_controller_spec.rb index 6e7475ed12..1a63077736 100644 --- a/spec/controllers/admin/export_domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/export_domain_blocks_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::ExportDomainBlocksController, type: :controller do +RSpec.describe Admin::ExportDomainBlocksController do render_views before do diff --git a/spec/controllers/admin/instances_controller_spec.rb b/spec/controllers/admin/instances_controller_spec.rb index 33174b9921..ce062085f4 100644 --- a/spec/controllers/admin/instances_controller_spec.rb +++ b/spec/controllers/admin/instances_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::InstancesController, type: :controller do +RSpec.describe Admin::InstancesController do render_views let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } diff --git a/spec/controllers/admin/reports/actions_controller_spec.rb b/spec/controllers/admin/reports/actions_controller_spec.rb index 4c2624a408..a84f2324e1 100644 --- a/spec/controllers/admin/reports/actions_controller_spec.rb +++ b/spec/controllers/admin/reports/actions_controller_spec.rb @@ -15,7 +15,7 @@ describe Admin::Reports::ActionsController do let(:report) { Fabricate(:report) } before do - post :preview, params: { report_id: report.id, action => '' } + post :preview, params: { :report_id => report.id, action => '' } end context 'when the action is "suspend"' do @@ -146,13 +146,13 @@ describe Admin::Reports::ActionsController do end end - context 'action as submit button' do + context 'with Action as submit button' do subject { post :create, params: common_params.merge({ action => '' }) } it_behaves_like 'all action types' end - context 'action as submit button' do + context 'with Action as submit button' do subject { post :create, params: common_params.merge({ moderation_action: action }) } it_behaves_like 'all action types' diff --git a/spec/controllers/admin/settings/branding_controller_spec.rb b/spec/controllers/admin/settings/branding_controller_spec.rb index ee1c441bc5..4b0f1e21e0 100644 --- a/spec/controllers/admin/settings/branding_controller_spec.rb +++ b/spec/controllers/admin/settings/branding_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::Settings::BrandingController, type: :controller do +RSpec.describe Admin::Settings::BrandingController do render_views describe 'When signed in as an admin' do diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb index 79d83db97d..872aed9998 100644 --- a/spec/controllers/admin/statuses_controller_spec.rb +++ b/spec/controllers/admin/statuses_controller_spec.rb @@ -30,7 +30,7 @@ describe Admin::StatusesController do end end - context 'filtering by media' do + context 'when filtering by media' do before do get :index, params: { account_id: account.id, media: '1' } end @@ -43,7 +43,7 @@ describe Admin::StatusesController do describe 'POST #batch' do before do - post :batch, params: { account_id: account.id, action => '', admin_status_batch_action: { status_ids: status_ids } } + post :batch, params: { :account_id => account.id, action => '', :admin_status_batch_action => { status_ids: status_ids } } end let(:status_ids) { [media_attached_status.id] } diff --git a/spec/controllers/admin/tags_controller_spec.rb b/spec/controllers/admin/tags_controller_spec.rb index 52fd09eb10..313298f14a 100644 --- a/spec/controllers/admin/tags_controller_spec.rb +++ b/spec/controllers/admin/tags_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::TagsController, type: :controller do +RSpec.describe Admin::TagsController do render_views before do diff --git a/spec/controllers/api/base_controller_spec.rb b/spec/controllers/api/base_controller_spec.rb index 080eab3c05..9f3ba81474 100644 --- a/spec/controllers/api/base_controller_spec.rb +++ b/spec/controllers/api/base_controller_spec.rb @@ -2,9 +2,11 @@ require 'rails_helper' -class FakeService; end - describe Api::BaseController do + before do + stub_const('FakeService', Class.new) + end + controller do def success head 200 @@ -72,7 +74,11 @@ describe Api::BaseController do end describe 'error handling' do - ERRORS_WITH_CODES = { + before do + routes.draw { get 'error' => 'api/base#error' } + end + + { ActiveRecord::RecordInvalid => 422, Mastodon::ValidationError => 422, ActiveRecord::RecordNotFound => 404, @@ -80,13 +86,7 @@ describe Api::BaseController do HTTP::Error => 503, OpenSSL::SSL::SSLError => 503, Mastodon::NotPermittedError => 403, - } - - before do - routes.draw { get 'error' => 'api/base#error' } - end - - ERRORS_WITH_CODES.each do |error, code| + }.each do |error, code| it "Handles error class of #{error}" do expect(FakeService).to receive(:new).and_raise(error) diff --git a/spec/controllers/api/oembed_controller_spec.rb b/spec/controllers/api/oembed_controller_spec.rb index 02875ee9f3..70248c3982 100644 --- a/spec/controllers/api/oembed_controller_spec.rb +++ b/spec/controllers/api/oembed_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::OEmbedController, type: :controller do +RSpec.describe Api::OEmbedController do render_views let(:alice) { Fabricate(:account, username: 'alice') } diff --git a/spec/controllers/api/v1/accounts/pins_controller_spec.rb b/spec/controllers/api/v1/accounts/pins_controller_spec.rb index 19bba093e4..b4aa9b7116 100644 --- a/spec/controllers/api/v1/accounts/pins_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/pins_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Accounts::PinsController, type: :controller do +RSpec.describe Api::V1::Accounts::PinsController do let(:john) { Fabricate(:user) } let(:kevin) { Fabricate(:user) } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: john.id, scopes: 'write:accounts') } diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb index da8d7fe3f0..6bc07fa9e0 100644 --- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb @@ -21,7 +21,7 @@ describe Api::V1::Accounts::RelationshipsController do lewis.follow!(user.account) end - context 'provided only one ID' do + context 'when provided only one ID' do before do get :index, params: { id: simon.id } end @@ -39,7 +39,7 @@ describe Api::V1::Accounts::RelationshipsController do end end - context 'provided multiple IDs' do + context 'when provided multiple IDs' do before do get :index, params: { id: [simon.id, lewis.id] } end diff --git a/spec/controllers/api/v1/accounts/search_controller_spec.rb b/spec/controllers/api/v1/accounts/search_controller_spec.rb index d2b675a3c8..aa9455a4a3 100644 --- a/spec/controllers/api/v1/accounts/search_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/search_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Accounts::SearchController, type: :controller do +RSpec.describe Api::V1::Accounts::SearchController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 5fbb650213..1cca69de73 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::AccountsController, type: :controller do +RSpec.describe Api::V1::AccountsController do render_views let(:user) { Fabricate(:user) } @@ -30,7 +30,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do post :create, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement } end - context 'given truthy agreement' do + context 'when given truthy agreement' do let(:agreement) { 'true' } it 'returns http success' do @@ -48,7 +48,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do end end - context 'given no agreement' do + context 'when given no agreement' do it 'returns http unprocessable entity' do expect(response).to have_http_status(422) end @@ -121,7 +121,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do end end - context 'modifying follow options' do + context 'when modifying follow options' do let(:locked) { false } before do diff --git a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb index cafbee212d..1c976455e1 100644 --- a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb +++ b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do +RSpec.describe Api::V1::Admin::AccountActionsController do render_views let(:role) { UserRole.find_by(name: 'Moderator') } diff --git a/spec/controllers/api/v1/admin/accounts_controller_spec.rb b/spec/controllers/api/v1/admin/accounts_controller_spec.rb index 9ffcdb34fb..852a521021 100644 --- a/spec/controllers/api/v1/admin/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/admin/accounts_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Admin::AccountsController, type: :controller do +RSpec.describe Api::V1::Admin::AccountsController do render_views let(:role) { UserRole.find_by(name: 'Moderator') } 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 15567907e4..9db8a35b46 100644 --- a/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb +++ b/spec/controllers/api/v1/admin/domain_allows_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Admin::DomainAllowsController, type: :controller do +RSpec.describe Api::V1::Admin::DomainAllowsController do render_views let(:role) { UserRole.find_by(name: 'Admin') } diff --git a/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb index 0460c701a4..5659843f7a 100644 --- a/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb +++ b/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do +RSpec.describe Api::V1::Admin::DomainBlocksController do render_views let(:role) { UserRole.find_by(name: 'Admin') } @@ -84,7 +84,7 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do BlockDomainService.new.call(domain_block) end - context 'downgrading a domain suspension to silence' do + context 'when downgrading a domain suspension to silence' do let(:original_severity) { 'suspend' } let(:new_severity) { 'silence' } @@ -101,7 +101,7 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do end end - context 'upgrading a domain silence to suspend' do + context 'when upgrading a domain silence to suspend' do let(:original_severity) { 'silence' } let(:new_severity) { 'suspend' } diff --git a/spec/controllers/api/v1/admin/reports_controller_spec.rb b/spec/controllers/api/v1/admin/reports_controller_spec.rb index 3d61fe5c3a..4f0c484e59 100644 --- a/spec/controllers/api/v1/admin/reports_controller_spec.rb +++ b/spec/controllers/api/v1/admin/reports_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Admin::ReportsController, type: :controller do +RSpec.describe Api::V1::Admin::ReportsController do render_views let(:role) { UserRole.find_by(name: 'Moderator') } diff --git a/spec/controllers/api/v1/announcements/reactions_controller_spec.rb b/spec/controllers/api/v1/announcements/reactions_controller_spec.rb index 25c52aa1d3..10aaa553f5 100644 --- a/spec/controllers/api/v1/announcements/reactions_controller_spec.rb +++ b/spec/controllers/api/v1/announcements/reactions_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Announcements::ReactionsController, type: :controller do +RSpec.describe Api::V1::Announcements::ReactionsController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/announcements_controller_spec.rb b/spec/controllers/api/v1/announcements_controller_spec.rb index eaab2abd80..15d94b4512 100644 --- a/spec/controllers/api/v1/announcements_controller_spec.rb +++ b/spec/controllers/api/v1/announcements_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::AnnouncementsController, type: :controller do +RSpec.describe Api::V1::AnnouncementsController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/apps_controller_spec.rb b/spec/controllers/api/v1/apps_controller_spec.rb index bde132c52e..de2a07f20a 100644 --- a/spec/controllers/api/v1/apps_controller_spec.rb +++ b/spec/controllers/api/v1/apps_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::AppsController, type: :controller do +RSpec.describe Api::V1::AppsController do render_views describe 'POST #create' do diff --git a/spec/controllers/api/v1/blocks_controller_spec.rb b/spec/controllers/api/v1/blocks_controller_spec.rb index a746389ca2..eaafc1b4fa 100644 --- a/spec/controllers/api/v1/blocks_controller_spec.rb +++ b/spec/controllers/api/v1/blocks_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::BlocksController, type: :controller do +RSpec.describe Api::V1::BlocksController do render_views let(:user) { Fabricate(:user) } @@ -13,13 +13,13 @@ RSpec.describe Api::V1::BlocksController, type: :controller do describe 'GET #index' do it 'limits according to limit parameter' do - 2.times.map { Fabricate(:block, account: user.account) } + Array.new(2) { Fabricate(:block, account: user.account) } get :index, params: { limit: 1 } expect(body_as_json.size).to eq 1 end it 'queries blocks in range according to max_id' do - blocks = 2.times.map { Fabricate(:block, account: user.account) } + blocks = Array.new(2) { Fabricate(:block, account: user.account) } get :index, params: { max_id: blocks[1] } @@ -28,7 +28,7 @@ RSpec.describe Api::V1::BlocksController, type: :controller do end it 'queries blocks in range according to since_id' do - blocks = 2.times.map { Fabricate(:block, account: user.account) } + blocks = Array.new(2) { Fabricate(:block, account: user.account) } get :index, params: { since_id: blocks[0] } @@ -37,7 +37,7 @@ RSpec.describe Api::V1::BlocksController, type: :controller do end it 'sets pagination header for next path' do - blocks = 2.times.map { Fabricate(:block, account: user.account) } + blocks = Array.new(2) { Fabricate(:block, account: user.account) } get :index, params: { limit: 1, since_id: blocks[0] } expect(response.headers['Link'].find_link(%w(rel next)).href).to eq api_v1_blocks_url(limit: 1, max_id: blocks[1]) end diff --git a/spec/controllers/api/v1/bookmarks_controller_spec.rb b/spec/controllers/api/v1/bookmarks_controller_spec.rb index bbf92c1539..69a37388ea 100644 --- a/spec/controllers/api/v1/bookmarks_controller_spec.rb +++ b/spec/controllers/api/v1/bookmarks_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::BookmarksController, type: :controller do +RSpec.describe Api::V1::BookmarksController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/conversations_controller_spec.rb b/spec/controllers/api/v1/conversations_controller_spec.rb index 36c4cb56f9..f888517154 100644 --- a/spec/controllers/api/v1/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/conversations_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::ConversationsController, type: :controller do +RSpec.describe Api::V1::ConversationsController do render_views let!(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) } diff --git a/spec/controllers/api/v1/custom_emojis_controller_spec.rb b/spec/controllers/api/v1/custom_emojis_controller_spec.rb index fe8daa7c5a..08af57f405 100644 --- a/spec/controllers/api/v1/custom_emojis_controller_spec.rb +++ b/spec/controllers/api/v1/custom_emojis_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::CustomEmojisController, type: :controller do +RSpec.describe Api::V1::CustomEmojisController do render_views describe 'GET #index' do diff --git a/spec/controllers/api/v1/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/domain_blocks_controller_spec.rb index 467ddbccce..aa98ec4c32 100644 --- a/spec/controllers/api/v1/domain_blocks_controller_spec.rb +++ b/spec/controllers/api/v1/domain_blocks_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::DomainBlocksController, type: :controller do +RSpec.describe Api::V1::DomainBlocksController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/emails/confirmations_controller_spec.rb b/spec/controllers/api/v1/emails/confirmations_controller_spec.rb index fc9843fef3..2a0703ed9c 100644 --- a/spec/controllers/api/v1/emails/confirmations_controller_spec.rb +++ b/spec/controllers/api/v1/emails/confirmations_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Emails::ConfirmationsController, type: :controller do +RSpec.describe Api::V1::Emails::ConfirmationsController do let(:confirmed_at) { nil } let(:user) { Fabricate(:user, confirmed_at: confirmed_at) } let(:app) { Fabricate(:application) } @@ -15,14 +15,14 @@ RSpec.describe Api::V1::Emails::ConfirmationsController, type: :controller do allow(controller).to receive(:doorkeeper_token) { token } end - context 'from a random app' do + context 'when from a random app' do it 'returns http forbidden' do post :create expect(response).to have_http_status(403) end end - context 'from an app that created the account' do + context 'when from an app that created the account' do before do user.update(created_by_application: token.application) end @@ -35,7 +35,7 @@ RSpec.describe Api::V1::Emails::ConfirmationsController, type: :controller do expect(response).to have_http_status(403) end - context 'but user changed e-mail and has not confirmed it' do + context 'with user changed e-mail and has not confirmed it' do before do user.update(email: 'foo@bar.com') end diff --git a/spec/controllers/api/v1/endorsements_controller_spec.rb b/spec/controllers/api/v1/endorsements_controller_spec.rb index ad5ff400f5..738804bb7b 100644 --- a/spec/controllers/api/v1/endorsements_controller_spec.rb +++ b/spec/controllers/api/v1/endorsements_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::EndorsementsController, type: :controller do +RSpec.describe Api::V1::EndorsementsController do let(:user) { Fabricate(:user) } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } diff --git a/spec/controllers/api/v1/favourites_controller_spec.rb b/spec/controllers/api/v1/favourites_controller_spec.rb index dd07bbb6e1..c9ca046be0 100644 --- a/spec/controllers/api/v1/favourites_controller_spec.rb +++ b/spec/controllers/api/v1/favourites_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::FavouritesController, type: :controller do +RSpec.describe Api::V1::FavouritesController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/filters_controller_spec.rb b/spec/controllers/api/v1/filters_controller_spec.rb index d583365cc1..8ccd2f4d66 100644 --- a/spec/controllers/api/v1/filters_controller_spec.rb +++ b/spec/controllers/api/v1/filters_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::FiltersController, type: :controller do +RSpec.describe Api::V1::FiltersController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/follow_requests_controller_spec.rb b/spec/controllers/api/v1/follow_requests_controller_spec.rb index 0220e02770..0a2c27d9ed 100644 --- a/spec/controllers/api/v1/follow_requests_controller_spec.rb +++ b/spec/controllers/api/v1/follow_requests_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::FollowRequestsController, type: :controller do +RSpec.describe Api::V1::FollowRequestsController do render_views let(:user) { Fabricate(:user, account_attributes: { locked: true }) } diff --git a/spec/controllers/api/v1/followed_tags_controller_spec.rb b/spec/controllers/api/v1/followed_tags_controller_spec.rb index e990065a9d..c1a366d4e3 100644 --- a/spec/controllers/api/v1/followed_tags_controller_spec.rb +++ b/spec/controllers/api/v1/followed_tags_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::FollowedTagsController, type: :controller do +RSpec.describe Api::V1::FollowedTagsController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/instances/activity_controller_spec.rb b/spec/controllers/api/v1/instances/activity_controller_spec.rb index 159792ee01..b446a521f8 100644 --- a/spec/controllers/api/v1/instances/activity_controller_spec.rb +++ b/spec/controllers/api/v1/instances/activity_controller_spec.rb @@ -2,14 +2,14 @@ require 'rails_helper' -RSpec.describe Api::V1::Instances::ActivityController, type: :controller do +RSpec.describe Api::V1::Instances::ActivityController do describe 'GET #show' do it 'returns 200' do get :show expect(response).to have_http_status(200) end - context '!Setting.activity_api_enabled' do + context 'with !Setting.activity_api_enabled' do it 'returns 404' do Setting.activity_api_enabled = false diff --git a/spec/controllers/api/v1/instances/peers_controller_spec.rb b/spec/controllers/api/v1/instances/peers_controller_spec.rb index 12a214a83a..92b1019154 100644 --- a/spec/controllers/api/v1/instances/peers_controller_spec.rb +++ b/spec/controllers/api/v1/instances/peers_controller_spec.rb @@ -2,14 +2,14 @@ require 'rails_helper' -RSpec.describe Api::V1::Instances::PeersController, type: :controller do +RSpec.describe Api::V1::Instances::PeersController do describe 'GET #index' do it 'returns 200' do get :index expect(response).to have_http_status(200) end - context '!Setting.peers_api_enabled' do + context 'with !Setting.peers_api_enabled' do it 'returns 404' do Setting.peers_api_enabled = false diff --git a/spec/controllers/api/v1/instances_controller_spec.rb b/spec/controllers/api/v1/instances_controller_spec.rb index 842669d965..fcc2c9288c 100644 --- a/spec/controllers/api/v1/instances_controller_spec.rb +++ b/spec/controllers/api/v1/instances_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::InstancesController, type: :controller do +RSpec.describe Api::V1::InstancesController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/lists/accounts_controller_spec.rb b/spec/controllers/api/v1/lists/accounts_controller_spec.rb index 337a5645c0..d4550dd769 100644 --- a/spec/controllers/api/v1/lists/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/lists/accounts_controller_spec.rb @@ -29,17 +29,48 @@ describe Api::V1::Lists::AccountsController do let(:scopes) { 'write:lists' } let(:bob) { Fabricate(:account, username: 'bob') } - before do - user.account.follow!(bob) - post :create, params: { list_id: list.id, account_ids: [bob.id] } + context 'when the added account is followed' do + before do + user.account.follow!(bob) + post :create, params: { list_id: list.id, account_ids: [bob.id] } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'adds account to the list' do + expect(list.accounts.include?(bob)).to be true + end end - it 'returns http success' do - expect(response).to have_http_status(200) + context 'when the added account has been sent a follow request' do + before do + user.account.follow_requests.create!(target_account: bob) + post :create, params: { list_id: list.id, account_ids: [bob.id] } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'adds account to the list' do + expect(list.accounts.include?(bob)).to be true + end end - it 'adds account to the list' do - expect(list.accounts.include?(bob)).to be true + context 'when the added account is not followed' do + before do + post :create, params: { list_id: list.id, account_ids: [bob.id] } + end + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + + it 'does not add the account to the list' do + expect(list.accounts.include?(bob)).to be false + end end end diff --git a/spec/controllers/api/v1/lists_controller_spec.rb b/spec/controllers/api/v1/lists_controller_spec.rb index f54d27e42b..15b9840ce1 100644 --- a/spec/controllers/api/v1/lists_controller_spec.rb +++ b/spec/controllers/api/v1/lists_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::ListsController, type: :controller do +RSpec.describe Api::V1::ListsController do render_views let!(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/markers_controller_spec.rb b/spec/controllers/api/v1/markers_controller_spec.rb index fb5f59a7cf..64e9dcafb6 100644 --- a/spec/controllers/api/v1/markers_controller_spec.rb +++ b/spec/controllers/api/v1/markers_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::MarkersController, type: :controller do +RSpec.describe Api::V1::MarkersController do render_views let!(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/media_controller_spec.rb b/spec/controllers/api/v1/media_controller_spec.rb index 90379dd92d..20d58e8c03 100644 --- a/spec/controllers/api/v1/media_controller_spec.rb +++ b/spec/controllers/api/v1/media_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::MediaController, type: :controller do +RSpec.describe Api::V1::MediaController do render_views let(:user) { Fabricate(:user) } @@ -37,7 +37,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do end end - context 'image/jpeg' do + context 'with image/jpeg' do before do post :create, params: { file: fixture_file_upload('attachment.jpg', 'image/jpeg') } end @@ -59,7 +59,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do end end - context 'image/gif' do + context 'with image/gif' do before do post :create, params: { file: fixture_file_upload('attachment.gif', 'image/gif') } end @@ -81,7 +81,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do end end - context 'video/webm' do + context 'with video/webm' do before do post :create, params: { file: fixture_file_upload('attachment.webm', 'video/webm') } end diff --git a/spec/controllers/api/v1/mutes_controller_spec.rb b/spec/controllers/api/v1/mutes_controller_spec.rb index 122d9d1c56..2645ed4e9d 100644 --- a/spec/controllers/api/v1/mutes_controller_spec.rb +++ b/spec/controllers/api/v1/mutes_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::MutesController, type: :controller do +RSpec.describe Api::V1::MutesController do render_views let(:user) { Fabricate(:user) } @@ -13,13 +13,13 @@ RSpec.describe Api::V1::MutesController, type: :controller do describe 'GET #index' do it 'limits according to limit parameter' do - 2.times.map { Fabricate(:mute, account: user.account) } + Array.new(2) { Fabricate(:mute, account: user.account) } get :index, params: { limit: 1 } expect(body_as_json.size).to eq 1 end it 'queries mutes in range according to max_id' do - mutes = 2.times.map { Fabricate(:mute, account: user.account) } + mutes = Array.new(2) { Fabricate(:mute, account: user.account) } get :index, params: { max_id: mutes[1] } @@ -28,7 +28,7 @@ RSpec.describe Api::V1::MutesController, type: :controller do end it 'queries mutes in range according to since_id' do - mutes = 2.times.map { Fabricate(:mute, account: user.account) } + mutes = Array.new(2) { Fabricate(:mute, account: user.account) } get :index, params: { since_id: mutes[0] } @@ -37,7 +37,7 @@ RSpec.describe Api::V1::MutesController, type: :controller do end it 'sets pagination header for next path' do - mutes = 2.times.map { Fabricate(:mute, account: user.account) } + mutes = Array.new(2) { Fabricate(:mute, account: user.account) } get :index, params: { limit: 1, since_id: mutes[0] } expect(response.headers['Link'].find_link(%w(rel next)).href).to eq api_v1_mutes_url(limit: 1, max_id: mutes[1]) end diff --git a/spec/controllers/api/v1/notifications_controller_spec.rb b/spec/controllers/api/v1/notifications_controller_spec.rb index f6cbd105e3..28b8e656ab 100644 --- a/spec/controllers/api/v1/notifications_controller_spec.rb +++ b/spec/controllers/api/v1/notifications_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::NotificationsController, type: :controller do +RSpec.describe Api::V1::NotificationsController do render_views let(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) } diff --git a/spec/controllers/api/v1/polls/votes_controller_spec.rb b/spec/controllers/api/v1/polls/votes_controller_spec.rb index 9d9b14e81c..7abd2a1b17 100644 --- a/spec/controllers/api/v1/polls/votes_controller_spec.rb +++ b/spec/controllers/api/v1/polls/votes_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Polls::VotesController, type: :controller do +RSpec.describe Api::V1::Polls::VotesController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/polls_controller_spec.rb b/spec/controllers/api/v1/polls_controller_spec.rb index 0602e44eef..3aae5496db 100644 --- a/spec/controllers/api/v1/polls_controller_spec.rb +++ b/spec/controllers/api/v1/polls_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::PollsController, type: :controller do +RSpec.describe Api::V1::PollsController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/reports_controller_spec.rb b/spec/controllers/api/v1/reports_controller_spec.rb index 06afaf0a75..0eb9ce1709 100644 --- a/spec/controllers/api/v1/reports_controller_spec.rb +++ b/spec/controllers/api/v1/reports_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::ReportsController, type: :controller do +RSpec.describe Api::V1::ReportsController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb index cac7b42b50..01816743e5 100644 --- a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :controller do +RSpec.describe Api::V1::Statuses::FavouritedByAccountsController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb index c5fedcefa3..756010af87 100644 --- a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb +++ b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controller do +RSpec.describe Api::V1::Statuses::RebloggedByAccountsController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb index f011bfd473..ab6f80a61b 100644 --- a/spec/controllers/api/v1/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/statuses_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::StatusesController, type: :controller do +RSpec.describe Api::V1::StatusesController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/suggestions_controller_spec.rb b/spec/controllers/api/v1/suggestions_controller_spec.rb index c99380c58b..c61ce0ec05 100644 --- a/spec/controllers/api/v1/suggestions_controller_spec.rb +++ b/spec/controllers/api/v1/suggestions_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::SuggestionsController, type: :controller do +RSpec.describe Api::V1::SuggestionsController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/tags_controller_spec.rb b/spec/controllers/api/v1/tags_controller_spec.rb index ed17a4fbfb..e914f5992d 100644 --- a/spec/controllers/api/v1/tags_controller_spec.rb +++ b/spec/controllers/api/v1/tags_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::TagsController, type: :controller do +RSpec.describe Api::V1::TagsController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v1/trends/tags_controller_spec.rb b/spec/controllers/api/v1/trends/tags_controller_spec.rb index d29551c56c..84370d8412 100644 --- a/spec/controllers/api/v1/trends/tags_controller_spec.rb +++ b/spec/controllers/api/v1/trends/tags_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V1::Trends::TagsController, type: :controller do +RSpec.describe Api::V1::Trends::TagsController do render_views describe 'GET #index' do diff --git a/spec/controllers/api/v2/admin/accounts_controller_spec.rb b/spec/controllers/api/v2/admin/accounts_controller_spec.rb index 5766fd549e..762c84af94 100644 --- a/spec/controllers/api/v2/admin/accounts_controller_spec.rb +++ b/spec/controllers/api/v2/admin/accounts_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V2::Admin::AccountsController, type: :controller do +RSpec.describe Api::V2::Admin::AccountsController do render_views let(:role) { UserRole.find_by(name: 'Moderator') } diff --git a/spec/controllers/api/v2/filters/keywords_controller_spec.rb b/spec/controllers/api/v2/filters/keywords_controller_spec.rb index 8c61059c64..057a9c3d00 100644 --- a/spec/controllers/api/v2/filters/keywords_controller_spec.rb +++ b/spec/controllers/api/v2/filters/keywords_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V2::Filters::KeywordsController, type: :controller do +RSpec.describe Api::V2::Filters::KeywordsController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v2/filters/statuses_controller_spec.rb b/spec/controllers/api/v2/filters/statuses_controller_spec.rb index 330cf45a60..588532ffd2 100644 --- a/spec/controllers/api/v2/filters/statuses_controller_spec.rb +++ b/spec/controllers/api/v2/filters/statuses_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V2::Filters::StatusesController, type: :controller do +RSpec.describe Api::V2::Filters::StatusesController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/api/v2/filters_controller_spec.rb b/spec/controllers/api/v2/filters_controller_spec.rb index 2b5610a4d6..722037eeb2 100644 --- a/spec/controllers/api/v2/filters_controller_spec.rb +++ b/spec/controllers/api/v2/filters_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V2::FiltersController, type: :controller do +RSpec.describe Api::V2::FiltersController do render_views let(:user) { Fabricate(:user) } @@ -66,7 +66,7 @@ RSpec.describe Api::V2::FiltersController, type: :controller do let!(:filter) { Fabricate(:custom_filter, account: user.account) } let!(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) } - context 'updating filter parameters' do + context 'when updating filter parameters' do before do put :update, params: { id: filter.id, title: 'updated', context: %w(home public) } end @@ -84,7 +84,7 @@ RSpec.describe Api::V2::FiltersController, type: :controller do end end - context 'updating keywords in bulk' do + context 'when updating keywords in bulk' do before do allow(redis).to receive_messages(publish: nil) put :update, params: { id: filter.id, keywords_attributes: [{ id: keyword.id, keyword: 'updated' }] } diff --git a/spec/controllers/api/v2/search_controller_spec.rb b/spec/controllers/api/v2/search_controller_spec.rb index d417ea58ca..bfabe8cc17 100644 --- a/spec/controllers/api/v2/search_controller_spec.rb +++ b/spec/controllers/api/v2/search_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Api::V2::SearchController, type: :controller do +RSpec.describe Api::V2::SearchController do render_views context 'with token' do diff --git a/spec/controllers/api/web/embeds_controller_spec.rb b/spec/controllers/api/web/embeds_controller_spec.rb index e03f5a3714..b0c48a5aed 100644 --- a/spec/controllers/api/web/embeds_controller_spec.rb +++ b/spec/controllers/api/web/embeds_controller_spec.rb @@ -10,10 +10,10 @@ describe Api::Web::EmbedsController do before { sign_in user } describe 'POST #create' do - subject(:response) { post :create, params: { url: url } } - subject(:body) { JSON.parse(response.body, symbolize_names: true) } + let(:response) { post :create, params: { url: url } } + context 'when successfully finds status' do let(:status) { Fabricate(:status) } let(:url) { "http://#{Rails.configuration.x.web_domain}/@#{status.account.username}/#{status.id}" } diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index bab17fa211..bbe6856e27 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe ApplicationController, type: :controller do +describe ApplicationController do controller do def success head 200 @@ -32,7 +32,7 @@ describe ApplicationController, type: :controller do end end - context 'forgery' do + context 'with a forgery' do subject do ActionController::Base.allow_forgery_protection = true routes.draw { post 'success' => 'anonymous#success' } @@ -112,7 +112,7 @@ describe ApplicationController, type: :controller do end end - context 'ActionController::RoutingError' do + context 'with ActionController::RoutingError' do subject do routes.draw { get 'routing_error' => 'anonymous#routing_error' } get 'routing_error' @@ -121,7 +121,7 @@ describe ApplicationController, type: :controller do include_examples 'respond_with_error', 404 end - context 'ActiveRecord::RecordNotFound' do + context 'with ActiveRecord::RecordNotFound' do subject do routes.draw { get 'record_not_found' => 'anonymous#record_not_found' } get 'record_not_found' @@ -130,7 +130,7 @@ describe ApplicationController, type: :controller do include_examples 'respond_with_error', 404 end - context 'ActionController::InvalidAuthenticityToken' do + context 'with ActionController::InvalidAuthenticityToken' do subject do routes.draw { get 'invalid_authenticity_token' => 'anonymous#invalid_authenticity_token' } get 'invalid_authenticity_token' @@ -230,14 +230,16 @@ describe ApplicationController, type: :controller do end describe 'cache_collection' do - class C < ApplicationController - public :cache_collection + subject do + Class.new(ApplicationController) do + public :cache_collection + end end shared_examples 'receives :with_includes' do |fabricator, klass| it 'uses raw if it is not an ActiveRecord::Relation' do record = Fabricate(fabricator) - expect(C.new.cache_collection([record], klass)).to eq [record] + expect(subject.new.cache_collection([record], klass)).to eq [record] end end @@ -248,16 +250,16 @@ describe ApplicationController, type: :controller do record = Fabricate(fabricator) relation = klass.none allow(relation).to receive(:cache_ids).and_return([record]) - expect(C.new.cache_collection(relation, klass)).to eq [record] + expect(subject.new.cache_collection(relation, klass)).to eq [record] end end it 'returns raw unless class responds to :with_includes' do raw = Object.new - expect(C.new.cache_collection(raw, Object)).to eq raw + expect(subject.new.cache_collection(raw, Object)).to eq raw end - context 'Status' do + context 'with a Status' do include_examples 'cacheable', :status, Status end end diff --git a/spec/controllers/auth/challenges_controller_spec.rb b/spec/controllers/auth/challenges_controller_spec.rb index 2a6ca301ef..32bbedde63 100644 --- a/spec/controllers/auth/challenges_controller_spec.rb +++ b/spec/controllers/auth/challenges_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe Auth::ChallengesController, type: :controller do +describe Auth::ChallengesController do render_views let(:password) { 'foobar12345' } diff --git a/spec/controllers/auth/confirmations_controller_spec.rb b/spec/controllers/auth/confirmations_controller_spec.rb index 8469119d23..799d3857e4 100644 --- a/spec/controllers/auth/confirmations_controller_spec.rb +++ b/spec/controllers/auth/confirmations_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe Auth::ConfirmationsController, type: :controller do +describe Auth::ConfirmationsController do render_views describe 'GET #new' do diff --git a/spec/controllers/auth/passwords_controller_spec.rb b/spec/controllers/auth/passwords_controller_spec.rb index 1c6874f08c..38fbed27a7 100644 --- a/spec/controllers/auth/passwords_controller_spec.rb +++ b/spec/controllers/auth/passwords_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe Auth::PasswordsController, type: :controller do +describe Auth::PasswordsController do include Devise::Test::ControllerHelpers describe 'GET #new' do diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index 5c422bdffc..ad8465e2ac 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Auth::RegistrationsController, type: :controller do +RSpec.describe Auth::RegistrationsController do render_views shared_examples 'checks for enabled registrations' do |path| @@ -157,7 +157,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do end end - context 'approval-based registrations without invite' do + context 'with Approval-based registrations without invite' do subject do Setting.registrations_mode = 'approved' request.headers['Accept-Language'] = accept_language @@ -184,7 +184,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do end end - context 'approval-based registrations with expired invite' do + context 'with Approval-based registrations with expired invite' do subject do Setting.registrations_mode = 'approved' request.headers['Accept-Language'] = accept_language @@ -212,7 +212,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do end end - context 'approval-based registrations with valid invite and required invite text' do + context 'with Approval-based registrations with valid invite and required invite text' do subject do inviter = Fabricate(:user, confirmed_at: 2.days.ago) Setting.registrations_mode = 'approved' diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb index 58befa124c..5b7d5d5cd4 100644 --- a/spec/controllers/auth/sessions_controller_spec.rb +++ b/spec/controllers/auth/sessions_controller_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' require 'webauthn/fake_client' -RSpec.describe Auth::SessionsController, type: :controller do +RSpec.describe Auth::SessionsController do render_views before do @@ -51,8 +51,8 @@ RSpec.describe Auth::SessionsController, type: :controller do end describe 'POST #create' do - context 'using PAM authentication', if: ENV['PAM_ENABLED'] == 'true' do - context 'using a valid password' do + context 'when using PAM authentication', if: ENV['PAM_ENABLED'] == 'true' do + context 'when using a valid password' do before do post :create, params: { user: { email: 'pam_user1', password: '123456' } } end @@ -66,7 +66,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using an invalid password' do + context 'when using an invalid password' do before do post :create, params: { user: { email: 'pam_user1', password: 'WRONGPW' } } end @@ -80,7 +80,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using a valid email and existing user' do + context 'when using a valid email and existing user' do let!(:user) do account = Fabricate.build(:account, username: 'pam_user1', user: nil) account.save!(validate: false) @@ -102,10 +102,10 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using password authentication' do + context 'when using password authentication' do let(:user) { Fabricate(:user, email: 'foo@bar.com', password: 'abcdefgh') } - context 'using a valid password' do + context 'when using a valid password' do before do post :create, params: { user: { email: user.email, password: user.password } } end @@ -119,7 +119,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using a valid password on a previously-used account with a new IP address' do + context 'when using a valid password on a previously-used account with a new IP address' do let(:previous_ip) { '1.2.3.4' } let(:current_ip) { '4.3.2.1' } @@ -145,7 +145,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using email with uppercase letters' do + context 'when using email with uppercase letters' do before do post :create, params: { user: { email: user.email.upcase, password: user.password } } end @@ -159,7 +159,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using an invalid password' do + context 'when using an invalid password' do before do post :create, params: { user: { email: user.email, password: 'wrongpw' } } end @@ -173,7 +173,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using an unconfirmed password' do + context 'when using an unconfirmed password' do before do request.headers['Accept-Language'] = accept_language post :create, params: { user: { email: unconfirmed_user.email, password: unconfirmed_user.password } } @@ -187,14 +187,14 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context "logging in from the user's page" do + context "when logging in from the user's page" do before do allow(controller).to receive(:single_user_mode?).and_return(single_user_mode) allow(controller).to receive(:stored_location_for).with(:user).and_return("/@#{user.account.username}") post :create, params: { user: { email: user.email, password: user.password } } end - context 'in single user mode' do + context 'with single user mode' do let(:single_user_mode) { true } it 'redirects to home' do @@ -202,7 +202,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'in non-single user mode' do + context 'with non-single user mode' do let(:single_user_mode) { false } it "redirects back to the user's page" do @@ -212,7 +212,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using two-factor authentication' do + context 'when using two-factor authentication' do context 'with OTP enabled as second factor' do let!(:user) do Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) @@ -224,7 +224,7 @@ RSpec.describe Auth::SessionsController, type: :controller do return codes end - context 'using email and password' do + context 'when using email and password' do before do post :create, params: { user: { email: user.email, password: user.password } } end @@ -235,7 +235,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using email and password after an unfinished log-in attempt to a 2FA-protected account' do + context 'when using email and password after an unfinished log-in attempt to a 2FA-protected account' do let!(:other_user) do Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) end @@ -251,7 +251,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using upcase email and password' do + context 'when using upcase email and password' do before do post :create, params: { user: { email: user.email.upcase, password: user.password } } end @@ -262,7 +262,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using a valid OTP' do + context 'when using a valid OTP' do before do post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } end @@ -291,7 +291,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using a valid recovery code' do + context 'when using a valid recovery code' do before do post :create, params: { user: { otp_attempt: recovery_codes.first } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } end @@ -305,7 +305,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using an invalid OTP' do + context 'when using an invalid OTP' do before do post :create, params: { user: { otp_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } end @@ -353,7 +353,7 @@ RSpec.describe Auth::SessionsController, type: :controller do let(:fake_credential) { fake_client.get(challenge: challenge, sign_count: sign_count) } - context 'using email and password' do + context 'when using email and password' do before do post :create, params: { user: { email: user.email, password: user.password } } end @@ -364,7 +364,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using upcase email and password' do + context 'when using upcase email and password' do before do post :create, params: { user: { email: user.email.upcase, password: user.password } } end @@ -375,7 +375,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using a valid webauthn credential' do + context 'when using a valid webauthn credential' do before do @controller.session[:webauthn_challenge] = challenge diff --git a/spec/controllers/concerns/account_controller_concern_spec.rb b/spec/controllers/concerns/account_controller_concern_spec.rb index 99975f4c44..57fc6f9653 100644 --- a/spec/controllers/concerns/account_controller_concern_spec.rb +++ b/spec/controllers/concerns/account_controller_concern_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe ApplicationController, type: :controller do +describe ApplicationController do controller do include AccountControllerConcern diff --git a/spec/controllers/concerns/accountable_concern_spec.rb b/spec/controllers/concerns/accountable_concern_spec.rb index 5c5180bc24..3c10082c34 100644 --- a/spec/controllers/concerns/accountable_concern_spec.rb +++ b/spec/controllers/concerns/accountable_concern_spec.rb @@ -3,18 +3,20 @@ require 'rails_helper' RSpec.describe AccountableConcern do - class Hoge - include AccountableConcern - attr_reader :current_account + let(:hoge_class) do + Class.new do + include AccountableConcern + attr_reader :current_account - def initialize(current_account) - @current_account = current_account + def initialize(current_account) + @current_account = current_account + end end end let(:user) { Fabricate(:account) } let(:target) { Fabricate(:account) } - let(:hoge) { Hoge.new(user) } + let(:hoge) { hoge_class.new(user) } describe '#log_action' do it 'creates Admin::ActionLog' do diff --git a/spec/controllers/concerns/cache_concern_spec.rb b/spec/controllers/concerns/cache_concern_spec.rb index a34d7d7267..bf328d679d 100644 --- a/spec/controllers/concerns/cache_concern_spec.rb +++ b/spec/controllers/concerns/cache_concern_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe CacheConcern, type: :controller do +RSpec.describe CacheConcern do controller(ApplicationController) do include CacheConcern @@ -23,14 +23,14 @@ RSpec.describe CacheConcern, type: :controller do end describe '#cache_collection' do - context 'given an empty array' do + context 'when given an empty array' do it 'returns an empty array' do get :empty_array expect(response.body).to eq '0' end end - context 'given an empty relation' do + context 'when given an empty relation' do it 'returns an empty array' do get :empty_relation expect(response.body).to eq '0' diff --git a/spec/controllers/concerns/challengable_concern_spec.rb b/spec/controllers/concerns/challengable_concern_spec.rb index 4db3b740db..3324bdd24f 100644 --- a/spec/controllers/concerns/challengable_concern_spec.rb +++ b/spec/controllers/concerns/challengable_concern_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ChallengableConcern, type: :controller do +RSpec.describe ChallengableConcern do controller(ApplicationController) do include ChallengableConcern @@ -31,7 +31,7 @@ RSpec.describe ChallengableConcern, type: :controller do sign_in user end - context 'for GET requests' do + context 'with GET requests' do before { get :foo } it 'does not ask for password' do @@ -39,7 +39,7 @@ RSpec.describe ChallengableConcern, type: :controller do end end - context 'for POST requests' do + context 'with POST requests' do before { post :bar } it 'does not ask for password' do @@ -56,7 +56,7 @@ RSpec.describe ChallengableConcern, type: :controller do sign_in user end - context 'for GET requests' do + context 'with GET requests' do before { get :foo, session: { challenge_passed_at: Time.now.utc } } it 'does not ask for password' do @@ -64,7 +64,7 @@ RSpec.describe ChallengableConcern, type: :controller do end end - context 'for POST requests' do + context 'with POST requests' do before { post :bar, session: { challenge_passed_at: Time.now.utc } } it 'does not ask for password' do @@ -81,7 +81,7 @@ RSpec.describe ChallengableConcern, type: :controller do sign_in user end - context 'for GET requests' do + context 'with GET requests' do before { get :foo } it 'renders challenge' do @@ -91,7 +91,7 @@ RSpec.describe ChallengableConcern, type: :controller do # See Auth::ChallengesControllerSpec end - context 'for POST requests' do + context 'with POST requests' do before { post :bar } it 'renders challenge' do diff --git a/spec/controllers/concerns/export_controller_concern_spec.rb b/spec/controllers/concerns/export_controller_concern_spec.rb index 003fd17f6f..b380246e7e 100644 --- a/spec/controllers/concerns/export_controller_concern_spec.rb +++ b/spec/controllers/concerns/export_controller_concern_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe ApplicationController, type: :controller do +describe ApplicationController do controller do include ExportControllerConcern diff --git a/spec/controllers/concerns/localized_spec.rb b/spec/controllers/concerns/localized_spec.rb index a89e24af04..caac94ea95 100644 --- a/spec/controllers/concerns/localized_spec.rb +++ b/spec/controllers/concerns/localized_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe ApplicationController, type: :controller do +describe ApplicationController do controller do include Localized @@ -41,7 +41,7 @@ describe ApplicationController, type: :controller do end end - context 'user with valid locale has signed in' do + context 'with a user with valid locale has signed in' do it "sets user's locale" do user = Fabricate(:user, locale: :ca) @@ -52,7 +52,7 @@ describe ApplicationController, type: :controller do end end - context 'user with invalid locale has signed in' do + context 'with a user with invalid locale has signed in' do before do user = Fabricate.build(:user, locale: :invalid) user.save!(validate: false) @@ -62,7 +62,7 @@ describe ApplicationController, type: :controller do include_examples 'default locale' end - context 'user has not signed in' do + context 'with a user who has not signed in' do include_examples 'default locale' end end diff --git a/spec/controllers/concerns/rate_limit_headers_spec.rb b/spec/controllers/concerns/rate_limit_headers_spec.rb index 00a9a2080d..7e1f92546d 100644 --- a/spec/controllers/concerns/rate_limit_headers_spec.rb +++ b/spec/controllers/concerns/rate_limit_headers_spec.rb @@ -16,7 +16,7 @@ describe ApplicationController do end describe 'rate limiting' do - context 'throttling is off' do + context 'when throttling is off' do before do request.env['rack.attack.throttle_data'] = nil end @@ -30,7 +30,7 @@ describe ApplicationController do end end - context 'throttling is on' do + context 'when throttling is on' do let(:start_time) { DateTime.new(2017, 1, 1, 12, 0, 0).utc } before do diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb index 13655f3133..df20a5d7ef 100644 --- a/spec/controllers/concerns/signature_verification_spec.rb +++ b/spec/controllers/concerns/signature_verification_spec.rb @@ -2,15 +2,17 @@ require 'rails_helper' -describe ApplicationController, type: :controller do - class WrappedActor - attr_reader :wrapped_account +describe ApplicationController do + let(:wrapped_actor_class) do + Class.new do + attr_reader :wrapped_account - def initialize(wrapped_account) - @wrapped_account = wrapped_account + def initialize(wrapped_account) + @wrapped_account = wrapped_account + end + + delegate :uri, :keypair, to: :wrapped_account end - - delegate :uri, :keypair, to: :wrapped_account end controller do @@ -33,8 +35,8 @@ describe ApplicationController, type: :controller do before do routes.draw do - match via: [:get, :post], 'success' => 'anonymous#success' - match via: [:get, :post], 'signature_required' => 'anonymous#signature_required' + match :via => [:get, :post], 'success' => 'anonymous#success' + match :via => [:get, :post], 'signature_required' => 'anonymous#signature_required' end end @@ -93,7 +95,7 @@ describe ApplicationController, type: :controller do end context 'with a valid actor that is not an Account' do - let(:actor) { WrappedActor.new(author) } + let(:actor) { wrapped_actor_class.new(author) } before do get :success diff --git a/spec/controllers/concerns/user_tracking_concern_spec.rb b/spec/controllers/concerns/user_tracking_concern_spec.rb index b2548d5c00..8e272468b6 100644 --- a/spec/controllers/concerns/user_tracking_concern_spec.rb +++ b/spec/controllers/concerns/user_tracking_concern_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe ApplicationController, type: :controller do +describe ApplicationController do controller do include UserTrackingConcern diff --git a/spec/controllers/disputes/appeals_controller_spec.rb b/spec/controllers/disputes/appeals_controller_spec.rb index affe63c59b..d0e1cd3908 100644 --- a/spec/controllers/disputes/appeals_controller_spec.rb +++ b/spec/controllers/disputes/appeals_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Disputes::AppealsController, type: :controller do +RSpec.describe Disputes::AppealsController do render_views before { sign_in current_user, scope: :user } diff --git a/spec/controllers/disputes/strikes_controller_spec.rb b/spec/controllers/disputes/strikes_controller_spec.rb index 1d678875c6..f6d28fc09a 100644 --- a/spec/controllers/disputes/strikes_controller_spec.rb +++ b/spec/controllers/disputes/strikes_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Disputes::StrikesController, type: :controller do +RSpec.describe Disputes::StrikesController do render_views before { sign_in current_user, scope: :user } diff --git a/spec/controllers/emojis_controller_spec.rb b/spec/controllers/emojis_controller_spec.rb index 710d23d924..249dfd9d51 100644 --- a/spec/controllers/emojis_controller_spec.rb +++ b/spec/controllers/emojis_controller_spec.rb @@ -8,10 +8,10 @@ describe EmojisController do let(:emoji) { Fabricate(:custom_emoji) } describe 'GET #show' do - subject(:response) { get :show, params: { id: emoji.id, format: :json } } - subject(:body) { JSON.parse(response.body, symbolize_names: true) } + let(:response) { get :show, params: { id: emoji.id, format: :json } } + it 'returns the right response' do expect(response).to have_http_status 200 expect(body[:name]).to eq ':coolcat:' diff --git a/spec/controllers/follower_accounts_controller_spec.rb b/spec/controllers/follower_accounts_controller_spec.rb index 0551dfcdec..b5b8ff9cb4 100644 --- a/spec/controllers/follower_accounts_controller_spec.rb +++ b/spec/controllers/follower_accounts_controller_spec.rb @@ -39,10 +39,10 @@ describe FollowerAccountsController do end context 'when format is json' do - subject(:response) { get :index, params: { account_username: alice.username, page: page, format: :json } } - subject(:body) { response.parsed_body } + let(:response) { get :index, params: { account_username: alice.username, page: page, format: :json } } + context 'with page' do let(:page) { 1 } diff --git a/spec/controllers/following_accounts_controller_spec.rb b/spec/controllers/following_accounts_controller_spec.rb index b049df890f..d1efeec251 100644 --- a/spec/controllers/following_accounts_controller_spec.rb +++ b/spec/controllers/following_accounts_controller_spec.rb @@ -39,10 +39,10 @@ describe FollowingAccountsController do end context 'when format is json' do - subject(:response) { get :index, params: { account_username: alice.username, page: page, format: :json } } - subject(:body) { response.parsed_body } + let(:response) { get :index, params: { account_username: alice.username, page: page, format: :json } } + context 'with page' do let(:page) { 1 } diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index 0d3722920c..3ddc5691a9 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe HomeController, type: :controller do +RSpec.describe HomeController do render_views describe 'GET #index' do diff --git a/spec/controllers/instance_actors_controller_spec.rb b/spec/controllers/instance_actors_controller_spec.rb index 84a07d4970..8406094311 100644 --- a/spec/controllers/instance_actors_controller_spec.rb +++ b/spec/controllers/instance_actors_controller_spec.rb @@ -2,9 +2,9 @@ require 'rails_helper' -RSpec.describe InstanceActorsController, type: :controller do +RSpec.describe InstanceActorsController do describe 'GET #show' do - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } shared_examples 'shared behavior' do diff --git a/spec/controllers/intents_controller_spec.rb b/spec/controllers/intents_controller_spec.rb index 02b46ddc79..668d833ea7 100644 --- a/spec/controllers/intents_controller_spec.rb +++ b/spec/controllers/intents_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe IntentsController, type: :controller do +RSpec.describe IntentsController do render_views let(:user) { Fabricate(:user) } diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb index 6f087625c5..9b3ae251e6 100644 --- a/spec/controllers/oauth/authorizations_controller_spec.rb +++ b/spec/controllers/oauth/authorizations_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Oauth::AuthorizationsController, type: :controller do +RSpec.describe Oauth::AuthorizationsController do render_views let(:app) { Doorkeeper::Application.create!(name: 'test', redirect_uri: 'http://localhost/', scopes: 'read') } diff --git a/spec/controllers/oauth/tokens_controller_spec.rb b/spec/controllers/oauth/tokens_controller_spec.rb index 3804e035bb..973393bcf2 100644 --- a/spec/controllers/oauth/tokens_controller_spec.rb +++ b/spec/controllers/oauth/tokens_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Oauth::TokensController, type: :controller do +RSpec.describe Oauth::TokensController do describe 'POST #revoke' do let!(:user) { Fabricate(:user) } let!(:application) { Fabricate(:application, confidential: false) } diff --git a/spec/controllers/settings/applications_controller_spec.rb b/spec/controllers/settings/applications_controller_spec.rb index e12628a18b..c0a57380e8 100644 --- a/spec/controllers/settings/applications_controller_spec.rb +++ b/spec/controllers/settings/applications_controller_spec.rb @@ -50,7 +50,7 @@ describe Settings::ApplicationsController do end describe 'POST #create' do - context 'success (passed scopes as a String)' do + context 'when success (passed scopes as a String)' do def call_create post :create, params: { doorkeeper_application: { @@ -72,7 +72,7 @@ describe Settings::ApplicationsController do end end - context 'success (passed scopes as an Array)' do + context 'when success (passed scopes as an Array)' do def call_create post :create, params: { doorkeeper_application: { @@ -94,7 +94,7 @@ describe Settings::ApplicationsController do end end - context 'failure' do + context 'with failure request' do before do post :create, params: { doorkeeper_application: { @@ -117,7 +117,7 @@ describe Settings::ApplicationsController do end describe 'PATCH #update' do - context 'success' do + context 'when success' do let(:opts) do { website: 'https://foo.bar/', @@ -142,7 +142,7 @@ describe Settings::ApplicationsController do end end - context 'failure' do + context 'with failure request' do before do patch :update, params: { id: app.id, diff --git a/spec/controllers/settings/imports_controller_spec.rb b/spec/controllers/settings/imports_controller_spec.rb index 98ba897e41..76e1e4ecb0 100644 --- a/spec/controllers/settings/imports_controller_spec.rb +++ b/spec/controllers/settings/imports_controller_spec.rb @@ -2,16 +2,25 @@ require 'rails_helper' -RSpec.describe Settings::ImportsController, type: :controller do +RSpec.describe Settings::ImportsController do render_views + let(:user) { Fabricate(:user) } + before do - sign_in Fabricate(:user), scope: :user + sign_in user, scope: :user end - describe 'GET #show' do + describe 'GET #index' do + let!(:import) { Fabricate(:bulk_import, account: user.account) } + let!(:other_import) { Fabricate(:bulk_import) } + before do - get :show + get :index + end + + it 'assigns the expected imports' do + expect(assigns(:recent_imports)).to eq [import] end it 'returns http success' do @@ -23,31 +32,288 @@ RSpec.describe Settings::ImportsController, type: :controller do end end - describe 'POST #create' do - it 'redirects to settings path with successful following import' do - service = double(call: nil) - allow(ResolveAccountService).to receive(:new).and_return(service) - post :create, params: { - import: { - type: 'following', - data: fixture_file_upload('imports.txt'), - }, - } - - expect(response).to redirect_to(settings_import_path) + describe 'GET #show' do + before do + get :show, params: { id: bulk_import.id } end - it 'redirects to settings path with successful blocking import' do - service = double(call: nil) - allow(ResolveAccountService).to receive(:new).and_return(service) - post :create, params: { - import: { - type: 'blocking', - data: fixture_file_upload('imports.txt'), - }, - } + context 'with someone else\'s import' do + let(:bulk_import) { Fabricate(:bulk_import, state: :unconfirmed) } - expect(response).to redirect_to(settings_import_path) + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'with an already-confirmed import' do + let(:bulk_import) { Fabricate(:bulk_import, account: user.account, state: :in_progress) } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'with an unconfirmed import' do + let(:bulk_import) { Fabricate(:bulk_import, account: user.account, state: :unconfirmed) } + + it 'returns http success' do + expect(response).to have_http_status(200) + end end end + + describe 'POST #confirm' do + subject { post :confirm, params: { id: bulk_import.id } } + + before do + allow(BulkImportWorker).to receive(:perform_async) + end + + context 'with someone else\'s import' do + let(:bulk_import) { Fabricate(:bulk_import, state: :unconfirmed) } + + it 'does not change the import\'s state' do + expect { subject }.to_not(change { bulk_import.reload.state }) + end + + it 'does not fire the import worker' do + subject + expect(BulkImportWorker).to_not have_received(:perform_async) + end + + it 'returns http not found' do + subject + expect(response).to have_http_status(404) + end + end + + context 'with an already-confirmed import' do + let(:bulk_import) { Fabricate(:bulk_import, account: user.account, state: :in_progress) } + + it 'does not change the import\'s state' do + expect { subject }.to_not(change { bulk_import.reload.state }) + end + + it 'does not fire the import worker' do + subject + expect(BulkImportWorker).to_not have_received(:perform_async) + end + + it 'returns http not found' do + subject + expect(response).to have_http_status(404) + end + end + + context 'with an unconfirmed import' do + let(:bulk_import) { Fabricate(:bulk_import, account: user.account, state: :unconfirmed) } + + it 'changes the import\'s state to scheduled' do + expect { subject }.to change { bulk_import.reload.state.to_sym }.from(:unconfirmed).to(:scheduled) + end + + it 'fires the import worker on the expected import' do + subject + expect(BulkImportWorker).to have_received(:perform_async).with(bulk_import.id) + end + + it 'redirects to imports path' do + subject + expect(response).to redirect_to(settings_imports_path) + end + end + end + + describe 'DELETE #destroy' do + subject { delete :destroy, params: { id: bulk_import.id } } + + context 'with someone else\'s import' do + let(:bulk_import) { Fabricate(:bulk_import, state: :unconfirmed) } + + it 'does not delete the import' do + expect { subject }.to_not(change { BulkImport.exists?(bulk_import.id) }) + end + + it 'returns http not found' do + subject + expect(response).to have_http_status(404) + end + end + + context 'with an already-confirmed import' do + let(:bulk_import) { Fabricate(:bulk_import, account: user.account, state: :in_progress) } + + it 'does not delete the import' do + expect { subject }.to_not(change { BulkImport.exists?(bulk_import.id) }) + end + + it 'returns http not found' do + subject + expect(response).to have_http_status(404) + end + end + + context 'with an unconfirmed import' do + let(:bulk_import) { Fabricate(:bulk_import, account: user.account, state: :unconfirmed) } + + it 'deletes the import' do + expect { subject }.to change { BulkImport.exists?(bulk_import.id) }.from(true).to(false) + end + + it 'redirects to imports path' do + subject + expect(response).to redirect_to(settings_imports_path) + end + end + end + + describe 'GET #failures' do + subject { get :failures, params: { id: bulk_import.id }, format: :csv } + + shared_examples 'export failed rows' do |expected_contents| + let(:bulk_import) { Fabricate(:bulk_import, account: user.account, type: import_type, state: :finished) } + + before do + bulk_import.update(total_items: bulk_import.rows.count, processed_items: bulk_import.rows.count, imported_items: 0) + end + + it 'returns http success' do + subject + expect(response).to have_http_status(200) + end + + it 'returns expected contents' do + subject + expect(response.body).to eq expected_contents + end + end + + context 'with follows' do + let(:import_type) { 'following' } + + let!(:rows) do + [ + { 'acct' => 'foo@bar' }, + { 'acct' => 'user@bar', 'show_reblogs' => false, 'notify' => true, 'languages' => ['fr', 'de'] }, + ].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) } + end + + include_examples 'export failed rows', "Account address,Show boosts,Notify on new posts,Languages\nfoo@bar,true,false,\nuser@bar,false,true,\"fr, de\"\n" + end + + context 'with blocks' do + let(:import_type) { 'blocking' } + + let!(:rows) do + [ + { 'acct' => 'foo@bar' }, + { 'acct' => 'user@bar' }, + ].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) } + end + + include_examples 'export failed rows', "foo@bar\nuser@bar\n" + end + + context 'with mutes' do + let(:import_type) { 'muting' } + + let!(:rows) do + [ + { 'acct' => 'foo@bar' }, + { 'acct' => 'user@bar', 'hide_notifications' => false }, + ].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) } + end + + include_examples 'export failed rows', "Account address,Hide notifications\nfoo@bar,true\nuser@bar,false\n" + end + + context 'with domain blocks' do + let(:import_type) { 'domain_blocking' } + + let!(:rows) do + [ + { 'domain' => 'bad.domain' }, + { 'domain' => 'evil.domain' }, + ].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) } + end + + include_examples 'export failed rows', "bad.domain\nevil.domain\n" + end + + context 'with bookmarks' do + let(:import_type) { 'bookmarks' } + + let!(:rows) do + [ + { 'uri' => 'https://foo.com/1' }, + { 'uri' => 'https://foo.com/2' }, + ].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) } + end + + include_examples 'export failed rows', "https://foo.com/1\nhttps://foo.com/2\n" + end + end + + describe 'POST #create' do + subject do + post :create, params: { + form_import: { + type: import_type, + mode: import_mode, + data: fixture_file_upload(import_file), + }, + } + end + + shared_examples 'successful import' do |type, file, mode| + let(:import_type) { type } + let(:import_file) { file } + let(:import_mode) { mode } + + it 'creates an unconfirmed bulk_import with expected type' do + expect { subject }.to change { user.account.bulk_imports.pluck(:state, :type) }.from([]).to([['unconfirmed', import_type]]) + end + + it 'redirects to confirmation page for the import' do + subject + expect(response).to redirect_to(settings_import_path(user.account.bulk_imports.first)) + end + end + + shared_examples 'unsuccessful import' do |type, file, mode| + let(:import_type) { type } + let(:import_file) { file } + let(:import_mode) { mode } + + it 'does not creates an unconfirmed bulk_import' do + expect { subject }.to_not(change { user.account.bulk_imports.count }) + end + + it 'sets error to the import' do + subject + expect(assigns(:import).errors).to_not be_empty + end + end + + it_behaves_like 'successful import', 'following', 'imports.txt', 'merge' + it_behaves_like 'successful import', 'following', 'imports.txt', 'overwrite' + it_behaves_like 'successful import', 'blocking', 'imports.txt', 'merge' + it_behaves_like 'successful import', 'blocking', 'imports.txt', 'overwrite' + it_behaves_like 'successful import', 'muting', 'imports.txt', 'merge' + it_behaves_like 'successful import', 'muting', 'imports.txt', 'overwrite' + it_behaves_like 'successful import', 'domain_blocking', 'domain_blocks.csv', 'merge' + it_behaves_like 'successful import', 'domain_blocking', 'domain_blocks.csv', 'overwrite' + it_behaves_like 'successful import', 'bookmarks', 'bookmark-imports.txt', 'merge' + it_behaves_like 'successful import', 'bookmarks', 'bookmark-imports.txt', 'overwrite' + + it_behaves_like 'unsuccessful import', 'following', 'domain_blocks.csv', 'merge' + it_behaves_like 'unsuccessful import', 'following', 'domain_blocks.csv', 'overwrite' + it_behaves_like 'unsuccessful import', 'blocking', 'domain_blocks.csv', 'merge' + it_behaves_like 'unsuccessful import', 'blocking', 'domain_blocks.csv', 'overwrite' + it_behaves_like 'unsuccessful import', 'muting', 'domain_blocks.csv', 'merge' + it_behaves_like 'unsuccessful import', 'muting', 'domain_blocks.csv', 'overwrite' + + it_behaves_like 'unsuccessful import', 'following', 'empty.csv', 'merge' + it_behaves_like 'unsuccessful import', 'following', 'empty.csv', 'overwrite' + end end diff --git a/spec/controllers/settings/preferences/appearance_controller_spec.rb b/spec/controllers/settings/preferences/appearance_controller_spec.rb index 083bf49544..9a98a41886 100644 --- a/spec/controllers/settings/preferences/appearance_controller_spec.rb +++ b/spec/controllers/settings/preferences/appearance_controller_spec.rb @@ -31,5 +31,11 @@ describe Settings::Preferences::AppearanceController do expect(response).to redirect_to(settings_preferences_appearance_path) end + + it 'renders show on failure' do + put :update, params: { user: { locale: 'fake option' } } + + expect(response).to render_template('preferences/appearance/show') + end end end diff --git a/spec/controllers/settings/preferences/base_controller_spec.rb b/spec/controllers/settings/preferences/base_controller_spec.rb new file mode 100644 index 0000000000..53b3a461ed --- /dev/null +++ b/spec/controllers/settings/preferences/base_controller_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Settings::Preferences::BaseController do + describe 'after_update_redirect_path' do + it 'raises error when called' do + expect { described_class.new.send(:after_update_redirect_path) }.to raise_error(/Override/) + end + end +end diff --git a/spec/controllers/settings/profiles_controller_spec.rb b/spec/controllers/settings/profiles_controller_spec.rb index 52ae1f5191..806fad19a8 100644 --- a/spec/controllers/settings/profiles_controller_spec.rb +++ b/spec/controllers/settings/profiles_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Settings::ProfilesController, type: :controller do +RSpec.describe Settings::ProfilesController do render_views let!(:user) { Fabricate(:user) } diff --git a/spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb index 719f70f16b..48dea62765 100644 --- a/spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb +++ b/spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb @@ -275,7 +275,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do end context 'when user have not enabled webauthn' do - context 'creation succeeds' do + context 'when creation succeeds' do it 'creates a webauthn credential' do @controller.session[:webauthn_challenge] = challenge diff --git a/spec/controllers/statuses_cleanup_controller_spec.rb b/spec/controllers/statuses_cleanup_controller_spec.rb index 693260f92b..e082b69c51 100644 --- a/spec/controllers/statuses_cleanup_controller_spec.rb +++ b/spec/controllers/statuses_cleanup_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe StatusesCleanupController, type: :controller do +RSpec.describe StatusesCleanupController do render_views before do diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index 4ac6a68bb2..c846dd1d63 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -72,7 +72,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http success' do @@ -97,7 +97,7 @@ describe StatusesController do end end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http success' do @@ -132,7 +132,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http not found' do @@ -140,7 +140,7 @@ describe StatusesController do end end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http not found' do @@ -156,7 +156,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http not found' do @@ -164,7 +164,7 @@ describe StatusesController do end end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http not found' do @@ -196,7 +196,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http success' do @@ -221,7 +221,7 @@ describe StatusesController do end end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http success' do @@ -260,7 +260,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http success' do @@ -285,7 +285,7 @@ describe StatusesController do end end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http success' do @@ -320,7 +320,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http not found' do @@ -328,7 +328,7 @@ describe StatusesController do end end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http not found' do @@ -347,7 +347,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http success' do @@ -372,7 +372,7 @@ describe StatusesController do end end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http success' do @@ -407,7 +407,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http not found' do @@ -415,7 +415,7 @@ describe StatusesController do end end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http not found' do @@ -460,7 +460,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http success' do @@ -485,7 +485,7 @@ describe StatusesController do end end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http success' do @@ -522,7 +522,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http success' do @@ -547,7 +547,7 @@ describe StatusesController do end end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http success' do @@ -582,7 +582,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http not found' do @@ -590,7 +590,7 @@ describe StatusesController do end end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http not found' do @@ -609,7 +609,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http success' do @@ -634,7 +634,7 @@ describe StatusesController do end end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http success' do @@ -669,7 +669,7 @@ describe StatusesController do get :show, params: { account_username: status.account.username, id: status.id, format: format } end - context 'as JSON' do + context 'with JSON' do let(:format) { 'json' } it 'returns http not found' do @@ -677,7 +677,7 @@ describe StatusesController do end end - context 'as HTML' do + context 'with HTML' do let(:format) { 'html' } it 'returns http not found' do diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb index 7a07801be7..d41e707d43 100644 --- a/spec/controllers/tags_controller_spec.rb +++ b/spec/controllers/tags_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe TagsController, type: :controller do +RSpec.describe TagsController do render_views describe 'GET #show' do diff --git a/spec/controllers/well_known/host_meta_controller_spec.rb b/spec/controllers/well_known/host_meta_controller_spec.rb index d537043708..4bd161cd9d 100644 --- a/spec/controllers/well_known/host_meta_controller_spec.rb +++ b/spec/controllers/well_known/host_meta_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe WellKnown::HostMetaController, type: :controller do +describe WellKnown::HostMetaController do render_views describe 'GET #show' do diff --git a/spec/controllers/well_known/nodeinfo_controller_spec.rb b/spec/controllers/well_known/nodeinfo_controller_spec.rb index f5cde150da..6ec34afd04 100644 --- a/spec/controllers/well_known/nodeinfo_controller_spec.rb +++ b/spec/controllers/well_known/nodeinfo_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe WellKnown::NodeInfoController, type: :controller do +describe WellKnown::NodeInfoController do render_views describe 'GET #index' do diff --git a/spec/controllers/well_known/webfinger_controller_spec.rb b/spec/controllers/well_known/webfinger_controller_spec.rb index 00103df706..8dc0f329b6 100644 --- a/spec/controllers/well_known/webfinger_controller_spec.rb +++ b/spec/controllers/well_known/webfinger_controller_spec.rb @@ -2,11 +2,11 @@ require 'rails_helper' -describe WellKnown::WebfingerController, type: :controller do +describe WellKnown::WebfingerController do render_views describe 'GET #show' do - subject do + subject(:perform_show!) do get :show, params: { resource: resource }, format: :json end @@ -45,7 +45,7 @@ describe WellKnown::WebfingerController, type: :controller do let(:resource) { alice.to_webfinger_s } before do - subject + perform_show! end it_behaves_like 'a successful response' @@ -56,7 +56,7 @@ describe WellKnown::WebfingerController, type: :controller do before do alice.suspend! - subject + perform_show! end it_behaves_like 'a successful response' @@ -68,7 +68,7 @@ describe WellKnown::WebfingerController, type: :controller do before do alice.suspend! alice.deletion_request.destroy - subject + perform_show! end it 'returns http gone' do @@ -80,7 +80,7 @@ describe WellKnown::WebfingerController, type: :controller do let(:resource) { 'acct:not@existing.com' } before do - subject + perform_show! end it 'returns http not found' do @@ -92,7 +92,7 @@ describe WellKnown::WebfingerController, type: :controller do let(:alternate_domains) { ['foo.org'] } before do - subject + perform_show! end context 'when an account exists' do @@ -116,11 +116,39 @@ describe WellKnown::WebfingerController, type: :controller do end end + context 'when the old name scheme is used to query the instance actor' do + let(:resource) do + "#{Rails.configuration.x.local_domain}@#{Rails.configuration.x.local_domain}" + end + + before do + perform_show! + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'does not set a Vary header' do + expect(response.headers['Vary']).to be_nil + end + + it 'returns application/jrd+json' do + expect(response.media_type).to eq 'application/jrd+json' + end + + it 'returns links for the internal account' do + json = body_as_json + expect(json[:subject]).to eq 'acct:mastodon.internal@cb6e6126.ngrok.io' + expect(json[:aliases]).to eq ['https://cb6e6126.ngrok.io/actor'] + end + end + context 'with no resource parameter' do let(:resource) { nil } before do - subject + perform_show! end it 'returns http bad request' do @@ -132,7 +160,7 @@ describe WellKnown::WebfingerController, type: :controller do let(:resource) { 'df/:dfkj' } before do - subject + perform_show! end it 'returns http bad request' do diff --git a/spec/fabricators/bulk_import_fabricator.rb b/spec/fabricators/bulk_import_fabricator.rb new file mode 100644 index 0000000000..673b7960d9 --- /dev/null +++ b/spec/fabricators/bulk_import_fabricator.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +Fabricator(:bulk_import) do + type 1 + state 1 + total_items 1 + processed_items 1 + imported_items 1 + finished_at '2022-11-18 14:55:07' + overwrite false + account +end diff --git a/spec/fabricators/bulk_import_row_fabricator.rb b/spec/fabricators/bulk_import_row_fabricator.rb new file mode 100644 index 0000000000..f8358e734d --- /dev/null +++ b/spec/fabricators/bulk_import_row_fabricator.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +Fabricator(:bulk_import_row) do + bulk_import + data '' +end diff --git a/spec/fixtures/files/empty.csv b/spec/fixtures/files/empty.csv new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spec/fixtures/files/following_accounts.csv b/spec/fixtures/files/following_accounts.csv new file mode 100644 index 0000000000..c7917443f8 --- /dev/null +++ b/spec/fixtures/files/following_accounts.csv @@ -0,0 +1,5 @@ +Account address,Show boosts,Notify on new posts,Languages + +user@example.com,true,false, + +user@test.com,true,true,"en,fr" diff --git a/spec/fixtures/files/muted_accounts.csv b/spec/fixtures/files/muted_accounts.csv new file mode 100644 index 0000000000..66f4315bce --- /dev/null +++ b/spec/fixtures/files/muted_accounts.csv @@ -0,0 +1,5 @@ +Account address,Hide notifications + +user@example.com,true + +user@test.com,false diff --git a/spec/helpers/accounts_helper_spec.rb b/spec/helpers/accounts_helper_spec.rb index 184b47dec6..2c949cde69 100644 --- a/spec/helpers/accounts_helper_spec.rb +++ b/spec/helpers/accounts_helper_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe AccountsHelper, type: :helper do +RSpec.describe AccountsHelper do def set_not_embedded_view params[:controller] = "not_#{StatusesHelper::EMBEDDED_CONTROLLER}" params[:action] = "not_#{StatusesHelper::EMBEDDED_ACTION}" diff --git a/spec/helpers/admin/account_moderation_notes_helper_spec.rb b/spec/helpers/admin/account_moderation_notes_helper_spec.rb index e01eba51da..6386f07ac9 100644 --- a/spec/helpers/admin/account_moderation_notes_helper_spec.rb +++ b/spec/helpers/admin/account_moderation_notes_helper_spec.rb @@ -2,11 +2,11 @@ require 'rails_helper' -RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do +RSpec.describe Admin::AccountModerationNotesHelper do include AccountsHelper describe '#admin_account_link_to' do - context 'account is nil' do + context 'when Account is nil' do let(:account) { nil } it 'returns nil' do @@ -30,7 +30,7 @@ RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do end describe '#admin_account_inline_link_to' do - context 'account is nil' do + context 'when Account is nil' do let(:account) { nil } it 'returns nil' do diff --git a/spec/helpers/admin/action_logs_helper_spec.rb b/spec/helpers/admin/action_logs_helper_spec.rb index 9d7ed4ab76..4e9d08f09d 100644 --- a/spec/helpers/admin/action_logs_helper_spec.rb +++ b/spec/helpers/admin/action_logs_helper_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe Admin::ActionLogsHelper, type: :helper do +RSpec.describe Admin::ActionLogsHelper do end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 5e2c4f7422..8bfca078de 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -124,6 +124,164 @@ describe ApplicationHelper do end end + describe 'available_sign_up_path' do + context 'when registrations are closed' do + before do + without_partial_double_verification do + allow(Setting).to receive(:registrations_mode).and_return('none') + end + end + + it 'redirects to joinmastodon site' do + expect(helper.available_sign_up_path).to match(/joinmastodon.org/) + end + end + + context 'when in omniauth only mode' do + around do |example| + ClimateControl.modify OMNIAUTH_ONLY: 'true' do + example.run + end + end + + it 'redirects to joinmastodon site' do + expect(helper.available_sign_up_path).to match(/joinmastodon.org/) + end + end + + context 'when registrations are allowed' do + it 'returns a link to the registration page' do + expect(helper.available_sign_up_path).to eq(new_user_registration_path) + end + end + end + + describe 'omniauth_only?' do + context 'when env var is set to true' do + around do |example| + ClimateControl.modify OMNIAUTH_ONLY: 'true' do + example.run + end + end + + it 'returns true' do + expect(helper).to be_omniauth_only + end + end + + context 'when env var is not set' do + around do |example| + ClimateControl.modify OMNIAUTH_ONLY: nil do + example.run + end + end + + it 'returns false' do + expect(helper).to_not be_omniauth_only + end + end + end + + describe 'quote_wrap' do + it 'indents and quote wraps text' do + text = <<~TEXT + Hello this is a nice message for you to quote. + Be careful because it has two lines. + TEXT + + expect(helper.quote_wrap(text)).to eq <<~EXPECTED.strip + > Hello this is a nice message for you to quote. + > Be careful because it has two lines. + EXPECTED + end + end + + describe 'storage_host' do + context 'when S3 alias is present' do + around do |example| + ClimateControl.modify S3_ALIAS_HOST: 's3.alias' do + example.run + end + end + + it 'returns true' do + expect(helper.storage_host).to eq('https://s3.alias') + end + end + + context 'when S3 cloudfront is present' do + around do |example| + ClimateControl.modify S3_CLOUDFRONT_HOST: 's3.cloudfront' do + example.run + end + end + + it 'returns true' do + expect(helper.storage_host).to eq('https://s3.cloudfront') + end + end + + context 'when neither env value is present' do + it 'returns false' do + expect(helper.storage_host).to eq('https:') + end + end + end + + describe 'storage_host?' do + context 'when S3 alias is present' do + around do |example| + ClimateControl.modify S3_ALIAS_HOST: 's3.alias' do + example.run + end + end + + it 'returns true' do + expect(helper.storage_host?).to be true + end + end + + context 'when S3 cloudfront is present' do + around do |example| + ClimateControl.modify S3_CLOUDFRONT_HOST: 's3.cloudfront' do + example.run + end + end + + it 'returns true' do + expect(helper.storage_host?).to be true + end + end + + context 'when neither env value is present' do + it 'returns false' do + expect(helper.storage_host?).to be false + end + end + end + + describe 'visibility_icon' do + it 'returns a globe icon for a public visible status' do + result = helper.visibility_icon Status.new(visibility: 'public') + expect(result).to match(/globe/) + end + + it 'returns an unlock icon for a unlisted visible status' do + result = helper.visibility_icon Status.new(visibility: 'unlisted') + expect(result).to match(/unlock/) + end + + it 'returns a lock icon for a private visible status' do + result = helper.visibility_icon Status.new(visibility: 'private') + expect(result).to match(/lock/) + end + + it 'returns an at icon for a direct visible status' do + result = helper.visibility_icon Status.new(visibility: 'direct') + expect(result).to match(/at/) + end + end + describe 'title' do around do |example| site_title = Setting.site_title diff --git a/spec/helpers/flashes_helper_spec.rb b/spec/helpers/flashes_helper_spec.rb index ea143eed72..035e8a1de0 100644 --- a/spec/helpers/flashes_helper_spec.rb +++ b/spec/helpers/flashes_helper_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe FlashesHelper, type: :helper do +describe FlashesHelper do describe 'user_facing_flashes' do it 'returns user facing flashes' do flash[:alert] = 'an alert' diff --git a/spec/helpers/formatting_helper_spec.rb b/spec/helpers/formatting_helper_spec.rb index af604a87b5..d6e7631f66 100644 --- a/spec/helpers/formatting_helper_spec.rb +++ b/spec/helpers/formatting_helper_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe FormattingHelper, type: :helper do +describe FormattingHelper do include Devise::Test::ControllerHelpers describe '#rss_status_content_format' do diff --git a/spec/helpers/home_helper_spec.rb b/spec/helpers/home_helper_spec.rb index 3d2c5fe248..15067471ed 100644 --- a/spec/helpers/home_helper_spec.rb +++ b/spec/helpers/home_helper_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe HomeHelper, type: :helper do +RSpec.describe HomeHelper do describe 'default_props' do it 'returns default properties according to the context' do expect(helper.default_props).to eq locale: I18n.locale diff --git a/spec/helpers/jsonld_helper_spec.rb b/spec/helpers/jsonld_helper_spec.rb index ddd4bfe629..3575bba859 100644 --- a/spec/helpers/jsonld_helper_spec.rb +++ b/spec/helpers/jsonld_helper_spec.rb @@ -22,14 +22,14 @@ describe JsonLdHelper do end describe '#first_of_value' do - context 'value.is_a?(Array)' do + context 'when value.is_a?(Array)' do it 'returns value.first' do value = ['a'] expect(helper.first_of_value(value)).to be 'a' end end - context '!value.is_a?(Array)' do + context 'with !value.is_a?(Array)' do it 'returns value' do value = 'a' expect(helper.first_of_value(value)).to be 'a' @@ -38,14 +38,14 @@ describe JsonLdHelper do end describe '#supported_context?' do - context "!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)" do + context 'when json is present and in an activitypub tagmanager context' do it 'returns true' do json = { '@context' => ActivityPub::TagManager::CONTEXT }.as_json expect(helper.supported_context?(json)).to be true end end - context 'else' do + context 'when not in activitypub tagmanager context' do it 'returns false' do json = nil expect(helper.supported_context?(json)).to be false @@ -90,7 +90,7 @@ describe JsonLdHelper do end end - context 'compaction and forwarding' do + context 'with compaction and forwarding' do let(:json) do { '@context' => [ diff --git a/spec/helpers/routing_helper_spec.rb b/spec/helpers/routing_helper_spec.rb index 940392c9b0..852d02cebc 100644 --- a/spec/helpers/routing_helper_spec.rb +++ b/spec/helpers/routing_helper_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe RoutingHelper, type: :helper do +RSpec.describe RoutingHelper do describe '.full_asset_url' do around do |example| use_s3 = Rails.configuration.x.use_s3 @@ -24,7 +24,7 @@ RSpec.describe RoutingHelper, type: :helper do end end - context 'Do not use S3' do + context 'when not using S3' do before do Rails.configuration.x.use_s3 = false end @@ -32,7 +32,7 @@ RSpec.describe RoutingHelper, type: :helper do it_behaves_like 'returns full path URL' end - context 'Use S3' do + context 'when using S3' do before do Rails.configuration.x.use_s3 = true end diff --git a/spec/lib/activitypub/activity/accept_spec.rb b/spec/lib/activitypub/activity/accept_spec.rb index 890a07be54..d6b6071279 100644 --- a/spec/lib/activitypub/activity/accept_spec.rb +++ b/spec/lib/activitypub/activity/accept_spec.rb @@ -43,7 +43,7 @@ RSpec.describe ActivityPub::Activity::Accept do end end - context 'given a relay' do + context 'when given a relay' do subject { described_class.new(json, sender) } let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') } diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb index 394b1d7b93..365861bcd8 100644 --- a/spec/lib/activitypub/activity/announce_spec.rb +++ b/spec/lib/activitypub/activity/announce_spec.rb @@ -39,7 +39,7 @@ RSpec.describe ActivityPub::Activity::Announce do subject.perform end - context 'a known status' do + context 'with known status' do let(:object_json) do ActivityPub::TagManager.instance.uri_for(status) end @@ -49,7 +49,7 @@ RSpec.describe ActivityPub::Activity::Announce do end end - context 'an unknown status' do + context 'with unknown status' do let(:object_json) { 'https://example.com/actor/hello-world' } it 'creates a reblog by sender of status' do @@ -60,7 +60,7 @@ RSpec.describe ActivityPub::Activity::Announce do end end - context 'self-boost of a previously unknown status with correct attributedTo' do + context 'when self-boost of a previously unknown status with correct attributedTo' do let(:object_json) do { id: 'https://example.com/actor#bar', @@ -76,7 +76,7 @@ RSpec.describe ActivityPub::Activity::Announce do end end - context 'self-boost of a previously unknown status with correct attributedTo, inlined Collection in audience' do + context 'when self-boost of a previously unknown status with correct attributedTo, inlined Collection in audience' do let(:object_json) do { id: 'https://example.com/actor#bar', @@ -123,7 +123,7 @@ RSpec.describe ActivityPub::Activity::Announce do stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json)) end - context 'and the relay is enabled' do + context 'when the relay is enabled' do before do relay.update(state: :accepted) subject.perform @@ -135,7 +135,7 @@ RSpec.describe ActivityPub::Activity::Announce do end end - context 'and the relay is disabled' do + context 'when the relay is disabled' do before do subject.perform end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 1226cfd8ef..d7c4c131a2 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -31,7 +31,7 @@ RSpec.describe ActivityPub::Activity::Create do subject.perform end - context 'object has been edited' do + context 'when object has been edited' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -57,7 +57,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'object has update date equal to creation date' do + context 'when object has update date equal to creation date' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -83,7 +83,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'unknown object type' do + context 'with an unknown object type' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -97,7 +97,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'standalone' do + context 'with a standalone' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -121,7 +121,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'public with explicit public address' do + context 'when public with explicit public address' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -139,7 +139,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'public with as:Public' do + context 'when public with as:Public' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -157,7 +157,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'public with Public' do + context 'when public with Public' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -175,7 +175,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'unlisted with explicit public address' do + context 'when unlisted with explicit public address' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -193,7 +193,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'unlisted with as:Public' do + context 'when unlisted with as:Public' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -211,7 +211,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'unlisted with Public' do + context 'when unlisted with Public' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -229,7 +229,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'private' do + context 'when private' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -247,7 +247,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'private with inlined Collection in audience' do + context 'when private with inlined Collection in audience' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -269,7 +269,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'limited' do + context 'when limited' do let(:recipient) { Fabricate(:account) } let(:object_json) do @@ -294,7 +294,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'limited when direct message assertion is false' do + context 'when directMessage attribute is false' do let(:recipient) { Fabricate(:account) } let(:object_json) do @@ -311,7 +311,7 @@ RSpec.describe ActivityPub::Activity::Create do } end - it 'creates status' do + it 'creates status with limited visibility' do status = sender.statuses.first expect(status).to_not be_nil @@ -319,7 +319,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'direct' do + context 'when direct' do let(:recipient) { Fabricate(:account) } let(:object_json) do @@ -335,7 +335,7 @@ RSpec.describe ActivityPub::Activity::Create do } end - it 'creates status' do + it 'creates status with direct visibility' do status = sender.statuses.first expect(status).to_not be_nil @@ -343,7 +343,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'direct when direct message assertion is true' do + context 'when directMessage attribute is true' do let(:recipient) { Fabricate(:account) } let(:object_json) do @@ -364,7 +364,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'as a reply' do + context 'with a reply' do let(:original_status) { Fabricate(:status) } let(:object_json) do diff --git a/spec/lib/activitypub/activity/follow_spec.rb b/spec/lib/activitypub/activity/follow_spec.rb index eb8b17d615..c1829cb8d7 100644 --- a/spec/lib/activitypub/activity/follow_spec.rb +++ b/spec/lib/activitypub/activity/follow_spec.rb @@ -20,7 +20,7 @@ RSpec.describe ActivityPub::Activity::Follow do subject { described_class.new(json, sender) } context 'with no prior follow' do - context 'unlocked account' do + context 'with an unlocked account' do before do subject.perform end @@ -35,7 +35,7 @@ RSpec.describe ActivityPub::Activity::Follow do end end - context 'silenced account following an unlocked account' do + context 'when silenced account following an unlocked account' do before do sender.touch(:silenced_at) subject.perform @@ -51,7 +51,7 @@ RSpec.describe ActivityPub::Activity::Follow do end end - context 'unlocked account muting the sender' do + context 'with an unlocked account muting the sender' do before do recipient.mute!(sender) subject.perform @@ -67,7 +67,7 @@ RSpec.describe ActivityPub::Activity::Follow do end end - context 'locked account' do + context 'when locked account' do before do recipient.update(locked: true) subject.perform @@ -89,7 +89,7 @@ RSpec.describe ActivityPub::Activity::Follow do sender.active_relationships.create!(target_account: recipient, uri: 'bar') end - context 'unlocked account' do + context 'with an unlocked account' do before do subject.perform end @@ -103,7 +103,7 @@ RSpec.describe ActivityPub::Activity::Follow do end end - context 'silenced account following an unlocked account' do + context 'when silenced account following an unlocked account' do before do sender.touch(:silenced_at) subject.perform @@ -118,7 +118,7 @@ RSpec.describe ActivityPub::Activity::Follow do end end - context 'unlocked account muting the sender' do + context 'with an unlocked account muting the sender' do before do recipient.mute!(sender) subject.perform @@ -133,7 +133,7 @@ RSpec.describe ActivityPub::Activity::Follow do end end - context 'locked account' do + context 'when locked account' do before do recipient.update(locked: true) subject.perform @@ -154,7 +154,7 @@ RSpec.describe ActivityPub::Activity::Follow do sender.follow_requests.create!(target_account: recipient, uri: 'bar') end - context 'silenced account following an unlocked account' do + context 'when silenced account following an unlocked account' do before do sender.touch(:silenced_at) subject.perform @@ -170,7 +170,7 @@ RSpec.describe ActivityPub::Activity::Follow do end end - context 'locked account' do + context 'when locked account' do before do recipient.update(locked: true) subject.perform diff --git a/spec/lib/activitypub/activity/reject_spec.rb b/spec/lib/activitypub/activity/reject_spec.rb index 5e0f09bfe8..0a4243cd16 100644 --- a/spec/lib/activitypub/activity/reject_spec.rb +++ b/spec/lib/activitypub/activity/reject_spec.rb @@ -27,7 +27,7 @@ RSpec.describe ActivityPub::Activity::Reject do describe '#perform' do subject { described_class.new(json, sender) } - context 'rejecting a pending follow request by target' do + context 'when rejecting a pending follow request by target' do before do Fabricate(:follow_request, account: recipient, target_account: sender) subject.perform @@ -42,7 +42,7 @@ RSpec.describe ActivityPub::Activity::Reject do end end - context 'rejecting a pending follow request by uri' do + context 'when rejecting a pending follow request by uri' do before do Fabricate(:follow_request, account: recipient, target_account: sender, uri: 'bar') subject.perform @@ -57,7 +57,7 @@ RSpec.describe ActivityPub::Activity::Reject do end end - context 'rejecting a pending follow request by uri only' do + context 'when rejecting a pending follow request by uri only' do let(:object_json) { 'bar' } before do @@ -74,7 +74,7 @@ RSpec.describe ActivityPub::Activity::Reject do end end - context 'rejecting an existing follow relationship by target' do + context 'when rejecting an existing follow relationship by target' do before do Fabricate(:follow, account: recipient, target_account: sender) subject.perform @@ -89,7 +89,7 @@ RSpec.describe ActivityPub::Activity::Reject do end end - context 'rejecting an existing follow relationship by uri' do + context 'when rejecting an existing follow relationship by uri' do before do Fabricate(:follow, account: recipient, target_account: sender, uri: 'bar') subject.perform @@ -104,7 +104,7 @@ RSpec.describe ActivityPub::Activity::Reject do end end - context 'rejecting an existing follow relationship by uri only' do + context 'when rejecting an existing follow relationship by uri only' do let(:object_json) { 'bar' } before do @@ -122,7 +122,7 @@ RSpec.describe ActivityPub::Activity::Reject do end end - context 'given a relay' do + context 'when given a relay' do subject { described_class.new(json, sender) } let!(:relay) { Fabricate(:relay, state: :pending, follow_activity_id: 'https://abc-123/456') } diff --git a/spec/lib/activitypub/adapter_spec.rb b/spec/lib/activitypub/adapter_spec.rb index b981ea9c68..f9f8b8dce0 100644 --- a/spec/lib/activitypub/adapter_spec.rb +++ b/spec/lib/activitypub/adapter_spec.rb @@ -3,43 +3,51 @@ require 'rails_helper' RSpec.describe ActivityPub::Adapter do - class TestObject < ActiveModelSerializers::Model - attributes :foo - end - - class TestWithBasicContextSerializer < ActivityPub::Serializer - attributes :foo - end - - class TestWithNamedContextSerializer < ActivityPub::Serializer - context :security - attributes :foo - end - - class TestWithNestedNamedContextSerializer < ActivityPub::Serializer - attributes :foo - - has_one :virtual_object, key: :baz, serializer: TestWithNamedContextSerializer - - def virtual_object - object + before do + test_object_class = Class.new(ActiveModelSerializers::Model) do + attributes :foo end - end + stub_const('TestObject', test_object_class) - class TestWithContextExtensionSerializer < ActivityPub::Serializer - context_extensions :sensitive - attributes :foo - end - - class TestWithNestedContextExtensionSerializer < ActivityPub::Serializer - context_extensions :manually_approves_followers - attributes :foo - - has_one :virtual_object, key: :baz, serializer: TestWithContextExtensionSerializer - - def virtual_object - object + test_with_basic_context_serializer = Class.new(ActivityPub::Serializer) do + attributes :foo end + stub_const('TestWithBasicContextSerializer', test_with_basic_context_serializer) + + test_with_named_context_serializer = Class.new(ActivityPub::Serializer) do + context :security + attributes :foo + end + stub_const('TestWithNamedContextSerializer', test_with_named_context_serializer) + + test_with_nested_named_context_serializer = Class.new(ActivityPub::Serializer) do + attributes :foo + + has_one :virtual_object, key: :baz, serializer: TestWithNamedContextSerializer + + def virtual_object + object + end + end + stub_const('TestWithNestedNamedContextSerializer', test_with_nested_named_context_serializer) + + test_with_context_extension_serializer = Class.new(ActivityPub::Serializer) do + context_extensions :sensitive + attributes :foo + end + stub_const('TestWithContextExtensionSerializer', test_with_context_extension_serializer) + + test_with_nested_context_extension_serializer = Class.new(ActivityPub::Serializer) do + context_extensions :manually_approves_followers + attributes :foo + + has_one :virtual_object, key: :baz, serializer: TestWithContextExtensionSerializer + + def virtual_object + object + end + end + stub_const('TestWithNestedContextExtensionSerializer', test_with_nested_context_extension_serializer) end describe '#serializable_hash' do diff --git a/spec/lib/advanced_text_formatter_spec.rb b/spec/lib/advanced_text_formatter_spec.rb index 8b27b56a15..f923852196 100644 --- a/spec/lib/advanced_text_formatter_spec.rb +++ b/spec/lib/advanced_text_formatter_spec.rb @@ -9,10 +9,10 @@ RSpec.describe AdvancedTextFormatter do let(:preloaded_accounts) { nil } let(:content_type) { 'text/markdown' } - context 'given a markdown source' do + context 'with a markdown source' do let(:content_type) { 'text/markdown' } - context 'given text containing plain text' do + context 'with text containing plain text' do let(:text) { 'text' } it 'paragraphizes the text' do @@ -20,7 +20,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given text containing line feeds' do + context 'with text containing line feeds' do let(:text) { "line\nfeed" } it 'removes line feeds' do @@ -28,7 +28,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given some inline code using backticks' do + context 'with some inline code using backticks' do let(:text) { 'test `foo` bar' } it 'formats code using <code>' do @@ -36,7 +36,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a block code' do + context 'with a block code' do let(:text) { "test\n\n```\nint main(void) {\n return 0; // https://joinmastodon.org/foo\n}\n```\n" } it 'formats code using <pre> and <code>' do @@ -52,7 +52,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a link in inline code using backticks' do + context 'with a link in inline code using backticks' do let(:text) { 'test `https://foo.bar/bar` bar' } it 'does not rewrite the link' do @@ -60,7 +60,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given text with a local-domain mention' do + context 'with text with a local-domain mention' do let(:text) { 'foo https://cb6e6126.ngrok.io/about/more' } it 'creates a link' do @@ -68,7 +68,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given text containing linkable mentions' do + context 'with text containing linkable mentions' do let(:preloaded_accounts) { [Fabricate(:account, username: 'alice')] } let(:text) { '@alice' } @@ -77,7 +77,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given text containing unlinkable mentions' do + context 'with text containing unlinkable mentions' do let(:preloaded_accounts) { [] } let(:text) { '@alice' } @@ -86,7 +86,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a stand-alone medium URL' do + context 'with a stand-alone medium URL' do let(:text) { 'https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4' } it 'matches the full URL' do @@ -94,7 +94,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a stand-alone google URL' do + context 'with a stand-alone google URL' do let(:text) { 'http://google.com' } it 'matches the full URL' do @@ -102,7 +102,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a stand-alone URL with a newer TLD' do + context 'with a stand-alone URL with a newer TLD' do let(:text) { 'http://example.gay' } it 'matches the full URL' do @@ -110,7 +110,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a stand-alone IDN URL' do + context 'with a stand-alone IDN URL' do let(:text) { 'https://nic.みんな/' } it 'matches the full URL' do @@ -122,7 +122,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a URL with a trailing period' do + context 'with a URL with a trailing period' do let(:text) { 'http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona. ' } it 'matches the full URL but not the period' do @@ -130,7 +130,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a URL enclosed with parentheses' do + context 'with a URL enclosed with parentheses' do let(:text) { '(http://google.com/)' } it 'matches the full URL but not the parentheses' do @@ -138,7 +138,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a URL with a trailing exclamation point' do + context 'with a URL with a trailing exclamation point' do let(:text) { 'http://www.google.com!' } it 'matches the full URL but not the exclamation point' do @@ -146,7 +146,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a URL with a trailing single quote' do + context 'with a URL with a trailing single quote' do let(:text) { "http://www.google.com'" } it 'matches the full URL but not the single quote' do @@ -155,7 +155,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a URL with a trailing angle bracket' do + context 'with a URL with a trailing angle bracket' do let(:text) { 'http://www.google.com>' } it 'matches the full URL but not the angle bracket' do @@ -163,7 +163,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a URL with a query string' do + context 'with a URL with a query string' do context 'with escaped unicode character' do let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' } @@ -196,7 +196,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a URL with parentheses in it' do + context 'with a URL with parentheses in it' do let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' } it 'matches the full URL' do @@ -204,7 +204,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a URL in quotation marks' do + context 'with a URL in quotation marks' do let(:text) { '"https://example.com/"' } it 'does not match the quotation marks' do @@ -212,7 +212,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a URL in angle brackets' do + context 'with a URL in angle brackets' do let(:text) { '<https://example.com/>' } it 'does not match the angle brackets' do @@ -220,7 +220,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given a URL containing unsafe code (XSS attack, invisible part)' do + context 'with a URL containing unsafe code (XSS attack, invisible part)' do let(:text) { 'http://example.com/blahblahblahblah/a<script>alert("Hello")</script>' } it 'does not include the HTML in the URL' do @@ -232,7 +232,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given text containing HTML code (script tag)' do + context 'with text containing HTML code (script tag)' do let(:text) { '<script>alert("Hello")</script>' } it 'does not include a script tag' do @@ -240,7 +240,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given text containing HTML (XSS attack)' do + context 'with text containing HTML (XSS attack)' do let(:text) { %q{<img src="javascript:alert('XSS');">} } it 'does not include the javascript' do @@ -248,7 +248,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given an invalid URL' do + context 'with an invalid URL' do let(:text) { 'http://www\.google\.com' } it 'outputs the raw URL' do @@ -256,7 +256,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given text containing a hashtag' do + context 'with text containing a hashtag' do let(:text) { '#hashtag' } it 'creates a hashtag link' do @@ -264,7 +264,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given text containing a hashtag with Unicode chars' do + context 'with text containing a hashtag with Unicode chars' do let(:text) { '#hashtagタグ' } it 'creates a hashtag link' do @@ -272,7 +272,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given text with a stand-alone xmpp: URI' do + context 'with text with a stand-alone xmpp: URI' do let(:text) { 'xmpp:user@instance.com' } it 'matches the full URI' do @@ -280,7 +280,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given text with an xmpp: URI with a query-string' do + context 'with text with an xmpp: URI with a query-string' do let(:text) { 'please join xmpp:muc@instance.com?join right now' } it 'matches the full URI' do @@ -288,7 +288,7 @@ RSpec.describe AdvancedTextFormatter do end end - context 'given text containing a magnet: URI' do + context 'with text containing a magnet: URI' do let(:text) { 'wikipedia gives this example of a magnet uri: magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a' } it 'matches the full URI' do diff --git a/spec/lib/connection_pool/shared_connection_pool_spec.rb b/spec/lib/connection_pool/shared_connection_pool_spec.rb index 1144645580..a2fe75f742 100644 --- a/spec/lib/connection_pool/shared_connection_pool_spec.rb +++ b/spec/lib/connection_pool/shared_connection_pool_spec.rb @@ -3,22 +3,24 @@ require 'rails_helper' describe ConnectionPool::SharedConnectionPool do - class MiniConnection - attr_reader :site + subject { described_class.new(size: 5, timeout: 5) { |site| mini_connection_class.new(site) } } - def initialize(site) - @site = site + let(:mini_connection_class) do + Class.new do + attr_reader :site + + def initialize(site) + @site = site + end end end - subject { described_class.new(size: 5, timeout: 5) { |site| MiniConnection.new(site) } } - describe '#with' do it 'runs a block with a connection' do block_run = false subject.with('foo') do |connection| - expect(connection).to be_a MiniConnection + expect(connection).to be_a mini_connection_class block_run = true end diff --git a/spec/lib/connection_pool/shared_timed_stack_spec.rb b/spec/lib/connection_pool/shared_timed_stack_spec.rb index f680c59667..04d550eec5 100644 --- a/spec/lib/connection_pool/shared_timed_stack_spec.rb +++ b/spec/lib/connection_pool/shared_timed_stack_spec.rb @@ -3,30 +3,32 @@ require 'rails_helper' describe ConnectionPool::SharedTimedStack do - class MiniConnection - attr_reader :site + subject { described_class.new(5) { |site| mini_connection_class.new(site) } } - def initialize(site) - @site = site + let(:mini_connection_class) do + Class.new do + attr_reader :site + + def initialize(site) + @site = site + end end end - subject { described_class.new(5) { |site| MiniConnection.new(site) } } - describe '#push' do it 'keeps the connection in the stack' do - subject.push(MiniConnection.new('foo')) + subject.push(mini_connection_class.new('foo')) expect(subject.size).to eq 1 end end describe '#pop' do it 'returns a connection' do - expect(subject.pop('foo')).to be_a MiniConnection + expect(subject.pop('foo')).to be_a mini_connection_class end it 'returns the same connection that was pushed in' do - connection = MiniConnection.new('foo') + connection = mini_connection_class.new('foo') subject.push(connection) expect(subject.pop('foo')).to be connection end @@ -36,8 +38,8 @@ describe ConnectionPool::SharedTimedStack do end it 'repurposes a connection for a different site when maximum amount is reached' do - 5.times { subject.push(MiniConnection.new('foo')) } - expect(subject.pop('bar')).to be_a MiniConnection + 5.times { subject.push(mini_connection_class.new('foo')) } + expect(subject.pop('bar')).to be_a mini_connection_class end end @@ -47,14 +49,14 @@ describe ConnectionPool::SharedTimedStack do end it 'returns false when there are connections on the stack' do - subject.push(MiniConnection.new('foo')) + subject.push(mini_connection_class.new('foo')) expect(subject.empty?).to be false end end describe '#size' do it 'returns the number of connections on the stack' do - 2.times { subject.push(MiniConnection.new('foo')) } + 2.times { subject.push(mini_connection_class.new('foo')) } expect(subject.size).to eq 2 end end diff --git a/spec/lib/emoji_formatter_spec.rb b/spec/lib/emoji_formatter_spec.rb index b73d5be4b9..e5accfbb0c 100644 --- a/spec/lib/emoji_formatter_spec.rb +++ b/spec/lib/emoji_formatter_spec.rb @@ -14,7 +14,7 @@ RSpec.describe EmojiFormatter do let(:emojis) { [emoji] } - context 'given text that is not marked as html-safe' do + context 'when given text that is not marked as html-safe' do let(:text) { 'Foo' } it 'raises an argument error' do @@ -22,7 +22,7 @@ RSpec.describe EmojiFormatter do end end - context 'given text with an emoji shortcode at the start' do + context 'when given text with an emoji shortcode at the start' do let(:text) { preformat_text(':coolcat: Beep boop') } it 'converts the shortcode to an image tag' do @@ -30,7 +30,7 @@ RSpec.describe EmojiFormatter do end end - context 'given text with an emoji shortcode in the middle' do + context 'when given text with an emoji shortcode in the middle' do let(:text) { preformat_text('Beep :coolcat: boop') } it 'converts the shortcode to an image tag' do @@ -38,7 +38,7 @@ RSpec.describe EmojiFormatter do end end - context 'given text with concatenated emoji shortcodes' do + context 'when given text with concatenated emoji shortcodes' do let(:text) { preformat_text(':coolcat::coolcat:') } it 'does not touch the shortcodes' do @@ -46,7 +46,7 @@ RSpec.describe EmojiFormatter do end end - context 'given text with an emoji shortcode at the end' do + context 'when given text with an emoji shortcode at the end' do let(:text) { preformat_text('Beep boop :coolcat:') } it 'converts the shortcode to an image tag' do diff --git a/spec/lib/entity_cache_spec.rb b/spec/lib/entity_cache_spec.rb index c750cddf3c..6d9afa4740 100644 --- a/spec/lib/entity_cache_spec.rb +++ b/spec/lib/entity_cache_spec.rb @@ -9,7 +9,7 @@ RSpec.describe EntityCache do describe '#emoji' do subject { EntityCache.instance.emoji(shortcodes, domain) } - context 'called with an empty list of shortcodes' do + context 'when called with an empty list of shortcodes' do let(:shortcodes) { [] } let(:domain) { 'example.org' } diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index d1e0d60e00..ccaa10dee9 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -27,7 +27,7 @@ RSpec.describe FeedManager do let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } let(:jeff) { Fabricate(:account, username: 'jeff') } - context 'for home feed' do + context 'with home feed' do it 'returns false for followee\'s status' do status = Fabricate(:status, text: 'Hello world', account: alice) bob.follow!(alice) @@ -162,7 +162,7 @@ RSpec.describe FeedManager do end end - context 'for mentions feed' do + context 'with mentions feed' do it 'returns true for status that mentions blocked account' do bob.block!(jeff) status = PostStatusService.new.call(alice, text: 'Hey @jeff') @@ -195,7 +195,7 @@ RSpec.describe FeedManager do it 'trims timelines if they will have more than FeedManager::MAX_ITEMS' do account = Fabricate(:account) status = Fabricate(:status) - members = FeedManager::MAX_ITEMS.times.map { |count| [count, count] } + members = Array.new(FeedManager::MAX_ITEMS) { |count| [count, count] } redis.zadd("feed:home:#{account.id}", members) FeedManager.instance.push_to_home(account, status) @@ -203,7 +203,7 @@ RSpec.describe FeedManager do expect(redis.zcard("feed:home:#{account.id}")).to eq FeedManager::MAX_ITEMS end - context 'reblogs' do + context 'with reblogs' do it 'saves reblogs of unseen statuses' do account = Fabricate(:account) reblogged = Fabricate(:status) @@ -240,7 +240,7 @@ RSpec.describe FeedManager do it 'does not save a new reblog of a recently-reblogged status' do account = Fabricate(:account) reblogged = Fabricate(:status) - reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) } + reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) } # The first reblog will be accepted FeedManager.instance.push_to_home(account, reblogs.first) @@ -269,7 +269,7 @@ RSpec.describe FeedManager do it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do account = Fabricate(:account) reblogged = Fabricate(:status) - reblogs = 3.times.map { Fabricate(:status, reblog: reblogged) } + reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) } # Accept the reblogs FeedManager.instance.push_to_home(account, reblogs[0]) @@ -285,7 +285,7 @@ RSpec.describe FeedManager do it 'saves a new reblog of a long-ago-reblogged status' do account = Fabricate(:account) reblogged = Fabricate(:status) - reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) } + reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) } # The first reblog will be accepted FeedManager.instance.push_to_home(account, reblogs.first) @@ -466,7 +466,7 @@ RSpec.describe FeedManager do it 'leaves a multiply-reblogged status if another reblog was in feed' do reblogged = Fabricate(:status) - reblogs = 3.times.map { Fabricate(:status, reblog: reblogged) } + reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) } reblogs.each do |reblog| FeedManager.instance.push_to_home(receiver, reblog) diff --git a/spec/lib/html_aware_formatter_spec.rb b/spec/lib/html_aware_formatter_spec.rb index 315035957b..a20902d4f9 100644 --- a/spec/lib/html_aware_formatter_spec.rb +++ b/spec/lib/html_aware_formatter_spec.rb @@ -18,7 +18,7 @@ RSpec.describe HtmlAwareFormatter do context 'when remote' do let(:local) { false } - context 'given plain text' do + context 'when given plain text' do let(:text) { 'Beep boop' } it 'keeps the plain text' do @@ -26,7 +26,7 @@ RSpec.describe HtmlAwareFormatter do end end - context 'given text containing script tags' do + context 'when given text containing script tags' do let(:text) { '<script>alert("Hello")</script>' } it 'strips the scripts' do @@ -34,7 +34,7 @@ RSpec.describe HtmlAwareFormatter do end end - context 'given text containing malicious classes' do + context 'when given text containing malicious classes' do let(:text) { '<span class="mention status__content__spoiler-link">Show more</span>' } it 'strips the malicious classes' do diff --git a/spec/lib/link_details_extractor_spec.rb b/spec/lib/link_details_extractor_spec.rb index a46dd743a9..ef501efbff 100644 --- a/spec/lib/link_details_extractor_spec.rb +++ b/spec/lib/link_details_extractor_spec.rb @@ -40,7 +40,7 @@ RSpec.describe LinkDetailsExtractor do context 'when structured data is present' do let(:original_url) { 'https://example.com/page.html' } - context 'and is wrapped in CDATA tags' do + context 'when is wrapped in CDATA tags' do let(:html) { <<~HTML } <!doctype html> <html> @@ -79,7 +79,7 @@ RSpec.describe LinkDetailsExtractor do end end - context 'but the first tag is invalid JSON' do + context 'with the first tag is invalid JSON' do let(:html) { <<~HTML } <!doctype html> <html> diff --git a/spec/lib/mastodon/settings_cli_spec.rb b/spec/lib/mastodon/settings_cli_spec.rb new file mode 100644 index 0000000000..713cb7e437 --- /dev/null +++ b/spec/lib/mastodon/settings_cli_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'mastodon/settings_cli' + +RSpec.describe Mastodon::SettingsCLI do + describe 'subcommand "registrations"' do + let(:cli) { Mastodon::RegistrationsCLI.new } + + before do + Setting.registrations_mode = nil + end + + describe '#open' do + it 'changes "registrations_mode" to "open"' do + expect { cli.open }.to change(Setting, :registrations_mode).from(nil).to('open') + end + + it 'displays success message' do + expect { cli.open }.to output( + a_string_including('OK') + ).to_stdout + end + end + + describe '#approved' do + it 'changes "registrations_mode" to "approved"' do + expect { cli.approved }.to change(Setting, :registrations_mode).from(nil).to('approved') + end + + it 'displays success message' do + expect { cli.approved }.to output( + a_string_including('OK') + ).to_stdout + end + + context 'with --require-reason' do + before do + cli.options = { require_reason: true } + end + + it 'changes "registrations_mode" to "approved"' do + expect { cli.approved }.to change(Setting, :registrations_mode).from(nil).to('approved') + end + + it 'sets "require_invite_text" to "true"' do + expect { cli.approved }.to change(Setting, :require_invite_text).from(false).to(true) + end + end + end + + describe '#close' do + it 'changes "registrations_mode" to "none"' do + expect { cli.close }.to change(Setting, :registrations_mode).from(nil).to('none') + end + + it 'displays success message' do + expect { cli.close }.to output( + a_string_including('OK') + ).to_stdout + end + end + end +end diff --git a/spec/lib/ostatus/tag_manager_spec.rb b/spec/lib/ostatus/tag_manager_spec.rb index 8104a7e791..fb9740ce3f 100644 --- a/spec/lib/ostatus/tag_manager_spec.rb +++ b/spec/lib/ostatus/tag_manager_spec.rb @@ -40,7 +40,7 @@ describe OStatus::TagManager do describe '#uri_for' do subject { OStatus::TagManager.instance.uri_for(target) } - context 'comment object' do + context 'with comment object' do let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) } it 'returns the unique tag for status' do @@ -49,7 +49,7 @@ describe OStatus::TagManager do end end - context 'note object' do + context 'with note object' do let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: false, thread: nil) } it 'returns the unique tag for status' do @@ -58,7 +58,7 @@ describe OStatus::TagManager do end end - context 'person object' do + context 'when person object' do let(:target) { Fabricate(:account, username: 'alice') } it 'returns the URL for account' do diff --git a/spec/lib/request_pool_spec.rb b/spec/lib/request_pool_spec.rb index 63dc9c5dd2..395268fe43 100644 --- a/spec/lib/request_pool_spec.rb +++ b/spec/lib/request_pool_spec.rb @@ -33,7 +33,7 @@ describe RequestPool do subject - threads = 20.times.map do |_i| + threads = Array.new(20) do |_i| Thread.new do 20.times do subject.with('http://example.com') do |http_client| diff --git a/spec/lib/scope_transformer_spec.rb b/spec/lib/scope_transformer_spec.rb index e5a992144d..8a9c7cf967 100644 --- a/spec/lib/scope_transformer_spec.rb +++ b/spec/lib/scope_transformer_spec.rb @@ -20,67 +20,67 @@ describe ScopeTransformer do end end - context 'for scope "read"' do + context 'with scope "read"' do let(:input) { 'read' } it_behaves_like 'a scope', nil, 'all', 'read' end - context 'for scope "write"' do + context 'with scope "write"' do let(:input) { 'write' } it_behaves_like 'a scope', nil, 'all', 'write' end - context 'for scope "follow"' do + context 'with scope "follow"' do let(:input) { 'follow' } it_behaves_like 'a scope', nil, 'follow', 'read/write' end - context 'for scope "crypto"' do + context 'with scope "crypto"' do let(:input) { 'crypto' } it_behaves_like 'a scope', nil, 'crypto', 'read/write' end - context 'for scope "push"' do + context 'with scope "push"' do let(:input) { 'push' } it_behaves_like 'a scope', nil, 'push', 'read/write' end - context 'for scope "admin:read"' do + context 'with scope "admin:read"' do let(:input) { 'admin:read' } it_behaves_like 'a scope', 'admin', 'all', 'read' end - context 'for scope "admin:write"' do + context 'with scope "admin:write"' do let(:input) { 'admin:write' } it_behaves_like 'a scope', 'admin', 'all', 'write' end - context 'for scope "admin:read:accounts"' do + context 'with scope "admin:read:accounts"' do let(:input) { 'admin:read:accounts' } it_behaves_like 'a scope', 'admin', 'accounts', 'read' end - context 'for scope "admin:write:accounts"' do + context 'with scope "admin:write:accounts"' do let(:input) { 'admin:write:accounts' } it_behaves_like 'a scope', 'admin', 'accounts', 'write' end - context 'for scope "read:accounts"' do + context 'with scope "read:accounts"' do let(:input) { 'read:accounts' } it_behaves_like 'a scope', nil, 'accounts', 'read' end - context 'for scope "write:accounts"' do + context 'with scope "write:accounts"' do let(:input) { 'write:accounts' } it_behaves_like 'a scope', nil, 'accounts', 'write' diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb index 5c78de7116..5b80ccb970 100644 --- a/spec/lib/status_cache_hydrator_spec.rb +++ b/spec/lib/status_cache_hydrator_spec.rb @@ -44,7 +44,7 @@ describe StatusCacheHydrator do let(:reblog) { Fabricate(:status) } let(:status) { Fabricate(:status, reblog: reblog) } - context 'that has been favourited' do + context 'when it has been favourited' do before do FavouriteService.new.call(account, reblog) end @@ -54,7 +54,7 @@ describe StatusCacheHydrator do end end - context 'that has been reblogged' do + context 'when it has been reblogged' do before do ReblogService.new.call(account, reblog) end @@ -64,7 +64,7 @@ describe StatusCacheHydrator do end end - context 'that has been pinned' do + context 'when it has been pinned' do let(:reblog) { Fabricate(:status, account: account) } before do @@ -76,7 +76,7 @@ describe StatusCacheHydrator do end end - context 'that has been followed tags' do + context 'when it has been followed tags' do let(:followed_tag) { Fabricate(:tag) } before do @@ -90,7 +90,7 @@ describe StatusCacheHydrator do end end - context 'that has a poll authored by the user' do + context 'when it has a poll authored by the user' do let(:poll) { Fabricate(:poll, account: account) } let(:reblog) { Fabricate(:status, poll: poll, account: account) } @@ -99,7 +99,7 @@ describe StatusCacheHydrator do end end - context 'that has been voted in' do + context 'when it has been voted in' do let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) } let(:reblog) { Fabricate(:status, poll: poll) } @@ -112,7 +112,7 @@ describe StatusCacheHydrator do end end - context 'that matches account filters' do + context 'when it matches account filters' do let(:reblog) { Fabricate(:status, text: 'this toot is about that banned word') } before do diff --git a/spec/lib/status_reach_finder_spec.rb b/spec/lib/status_reach_finder_spec.rb index 785ce28a0e..c53ac27992 100644 --- a/spec/lib/status_reach_finder_spec.rb +++ b/spec/lib/status_reach_finder_spec.rb @@ -4,7 +4,7 @@ require 'rails_helper' describe StatusReachFinder do describe '#inboxes' do - context 'for a local status' do + context 'with a local status' do subject { described_class.new(status) } let(:parent_status) { nil } diff --git a/spec/lib/text_formatter_spec.rb b/spec/lib/text_formatter_spec.rb index 3417b450c6..8b922c018b 100644 --- a/spec/lib/text_formatter_spec.rb +++ b/spec/lib/text_formatter_spec.rb @@ -8,7 +8,7 @@ RSpec.describe TextFormatter do let(:preloaded_accounts) { nil } - context 'given text containing plain text' do + context 'when given text containing plain text' do let(:text) { 'text' } it 'paragraphizes the text' do @@ -16,7 +16,7 @@ RSpec.describe TextFormatter do end end - context 'given text containing line feeds' do + context 'when given text containing line feeds' do let(:text) { "line\nfeed" } it 'removes line feeds' do @@ -24,7 +24,7 @@ RSpec.describe TextFormatter do end end - context 'given text containing linkable mentions' do + context 'when given text containing linkable mentions' do let(:preloaded_accounts) { [Fabricate(:account, username: 'alice')] } let(:text) { '@alice' } @@ -33,7 +33,7 @@ RSpec.describe TextFormatter do end end - context 'given text containing unlinkable mentions' do + context 'when given text containing unlinkable mentions' do let(:preloaded_accounts) { [] } let(:text) { '@alice' } @@ -42,7 +42,7 @@ RSpec.describe TextFormatter do end end - context 'given a stand-alone medium URL' do + context 'when given a stand-alone medium URL' do let(:text) { 'https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4' } it 'matches the full URL' do @@ -50,7 +50,7 @@ RSpec.describe TextFormatter do end end - context 'given a stand-alone google URL' do + context 'when given a stand-alone google URL' do let(:text) { 'http://google.com' } it 'matches the full URL' do @@ -58,7 +58,7 @@ RSpec.describe TextFormatter do end end - context 'given a stand-alone URL with a newer TLD' do + context 'when given a stand-alone URL with a newer TLD' do let(:text) { 'http://example.gay' } it 'matches the full URL' do @@ -66,7 +66,7 @@ RSpec.describe TextFormatter do end end - context 'given a stand-alone IDN URL' do + context 'when given a stand-alone IDN URL' do let(:text) { 'https://nic.みんな/' } it 'matches the full URL' do @@ -78,7 +78,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL with a trailing period' do + context 'when given a URL with a trailing period' do let(:text) { 'http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona. ' } it 'matches the full URL but not the period' do @@ -86,7 +86,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL enclosed with parentheses' do + context 'when given a URL enclosed with parentheses' do let(:text) { '(http://google.com/)' } it 'matches the full URL but not the parentheses' do @@ -94,7 +94,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL with a trailing exclamation point' do + context 'when given a URL with a trailing exclamation point' do let(:text) { 'http://www.google.com!' } it 'matches the full URL but not the exclamation point' do @@ -102,7 +102,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL with a trailing single quote' do + context 'when given a URL with a trailing single quote' do let(:text) { "http://www.google.com'" } it 'matches the full URL but not the single quote' do @@ -110,7 +110,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL with a trailing angle bracket' do + context 'when given a URL with a trailing angle bracket' do let(:text) { 'http://www.google.com>' } it 'matches the full URL but not the angle bracket' do @@ -118,7 +118,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL with a query string' do + context 'when given a URL with a query string' do context 'with escaped unicode character' do let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' } @@ -152,7 +152,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL with parentheses in it' do + context 'when given a URL with parentheses in it' do let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' } it 'matches the full URL' do @@ -160,7 +160,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL in quotation marks' do + context 'when given a URL in quotation marks' do let(:text) { '"https://example.com/"' } it 'does not match the quotation marks' do @@ -168,7 +168,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL in angle brackets' do + context 'when given a URL in angle brackets' do let(:text) { '<https://example.com/>' } it 'does not match the angle brackets' do @@ -176,7 +176,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL with Japanese path string' do + context 'when given a URL with Japanese path string' do let(:text) { 'https://ja.wikipedia.org/wiki/日本' } it 'matches the full URL' do @@ -184,7 +184,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL with Korean path string' do + context 'when given a URL with Korean path string' do let(:text) { 'https://ko.wikipedia.org/wiki/대한민국' } it 'matches the full URL' do @@ -192,7 +192,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL with a full-width space' do + context 'when given a URL with a full-width space' do let(:text) { 'https://example.com/ abc123' } it 'does not match the full-width space' do @@ -200,7 +200,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL in Japanese quotation marks' do + context 'when given a URL in Japanese quotation marks' do let(:text) { '「[https://example.org/」' } it 'does not match the quotation marks' do @@ -208,7 +208,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL with Simplified Chinese path string' do + context 'when given a URL with Simplified Chinese path string' do let(:text) { 'https://baike.baidu.com/item/中华人民共和国' } it 'matches the full URL' do @@ -216,7 +216,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL with Traditional Chinese path string' do + context 'when given a URL with Traditional Chinese path string' do let(:text) { 'https://zh.wikipedia.org/wiki/臺灣' } it 'matches the full URL' do @@ -224,7 +224,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL containing unsafe code (XSS attack, visible part)' do + context 'when given a URL containing unsafe code (XSS attack, visible part)' do let(:text) { 'http://example.com/b<del>b</del>' } it 'does not include the HTML in the URL' do @@ -236,7 +236,7 @@ RSpec.describe TextFormatter do end end - context 'given a URL containing unsafe code (XSS attack, invisible part)' do + context 'when given a URL containing unsafe code (XSS attack, invisible part)' do let(:text) { 'http://example.com/blahblahblahblah/a<script>alert("Hello")</script>' } it 'does not include the HTML in the URL' do @@ -248,7 +248,7 @@ RSpec.describe TextFormatter do end end - context 'given text containing HTML code (script tag)' do + context 'when given text containing HTML code (script tag)' do let(:text) { '<script>alert("Hello")</script>' } it 'escapes the HTML' do @@ -256,7 +256,7 @@ RSpec.describe TextFormatter do end end - context 'given text containing HTML (XSS attack)' do + context 'when given text containing HTML (XSS attack)' do let(:text) { %q{<img src="javascript:alert('XSS');">} } it 'escapes the HTML' do @@ -264,7 +264,7 @@ RSpec.describe TextFormatter do end end - context 'given an invalid URL' do + context 'when given an invalid URL' do let(:text) { 'http://www\.google\.com' } it 'outputs the raw URL' do @@ -272,7 +272,7 @@ RSpec.describe TextFormatter do end end - context 'given text containing a hashtag' do + context 'when given text containing a hashtag' do let(:text) { '#hashtag' } it 'creates a hashtag link' do @@ -280,7 +280,7 @@ RSpec.describe TextFormatter do end end - context 'given text containing a hashtag with Unicode chars' do + context 'when given text containing a hashtag with Unicode chars' do let(:text) { '#hashtagタグ' } it 'creates a hashtag link' do @@ -288,7 +288,7 @@ RSpec.describe TextFormatter do end end - context 'given text with a stand-alone xmpp: URI' do + context 'when given text with a stand-alone xmpp: URI' do let(:text) { 'xmpp:user@instance.com' } it 'matches the full URI' do @@ -296,7 +296,7 @@ RSpec.describe TextFormatter do end end - context 'given text with an xmpp: URI with a query-string' do + context 'when given text with an xmpp: URI with a query-string' do let(:text) { 'please join xmpp:muc@instance.com?join right now' } it 'matches the full URI' do @@ -304,7 +304,7 @@ RSpec.describe TextFormatter do end end - context 'given text containing a magnet: URI' do + context 'when given text containing a magnet: URI' do let(:text) { 'wikipedia gives this example of a magnet uri: magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a' } it 'matches the full URI' do diff --git a/spec/lib/vacuum/imports_vacuum_spec.rb b/spec/lib/vacuum/imports_vacuum_spec.rb new file mode 100644 index 0000000000..1e0abc5e01 --- /dev/null +++ b/spec/lib/vacuum/imports_vacuum_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Vacuum::ImportsVacuum do + subject { described_class.new } + + let!(:old_unconfirmed) { Fabricate(:bulk_import, state: :unconfirmed, created_at: 2.days.ago) } + let!(:new_unconfirmed) { Fabricate(:bulk_import, state: :unconfirmed, created_at: 10.seconds.ago) } + let!(:recent_ongoing) { Fabricate(:bulk_import, state: :in_progress, created_at: 20.minutes.ago) } + let!(:recent_finished) { Fabricate(:bulk_import, state: :finished, created_at: 1.day.ago) } + let!(:old_finished) { Fabricate(:bulk_import, state: :finished, created_at: 2.months.ago) } + + describe '#perform' do + it 'cleans up the expected imports' do + expect { subject.perform }.to change { BulkImport.all.pluck(:id) }.from([old_unconfirmed, new_unconfirmed, recent_ongoing, recent_finished, old_finished].map(&:id)).to([new_unconfirmed, recent_ongoing, recent_finished].map(&:id)) + end + end +end diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb index 132c6c7586..8e2eec40fd 100644 --- a/spec/mailers/admin_mailer_spec.rb +++ b/spec/mailers/admin_mailer_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe AdminMailer, type: :mailer do +RSpec.describe AdminMailer do describe '.new_report' do let(:sender) { Fabricate(:account, username: 'John') } let(:recipient) { Fabricate(:account, username: 'Mike') } diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb index ab98bac388..3113d05c37 100644 --- a/spec/mailers/notification_mailer_spec.rb +++ b/spec/mailers/notification_mailer_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe NotificationMailer, type: :mailer do +RSpec.describe NotificationMailer do let(:receiver) { Fabricate(:user) } let(:sender) { Fabricate(:account, username: 'bob') } let(:foreign_status) { Fabricate(:status, account: sender, text: 'The body of the foreign status') } diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 30824e7b4d..6144b2bbb8 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe UserMailer, type: :mailer do +describe UserMailer do let(:receiver) { Fabricate(:user) } shared_examples 'localized subject' do |*args, **kwrest| diff --git a/spec/models/account/field_spec.rb b/spec/models/account/field_spec.rb index 6745fbb261..5715a53791 100644 --- a/spec/models/account/field_spec.rb +++ b/spec/models/account/field_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Account::Field, type: :model do +RSpec.describe Account::Field do describe '#verified?' do subject { described_class.new(account, 'name' => 'Foo', 'value' => 'Bar', 'verified_at' => verified_at) } @@ -49,10 +49,10 @@ RSpec.describe Account::Field, type: :model do let(:account) { double('Account', local?: local) } - context 'for local accounts' do + context 'with local accounts' do let(:local) { true } - context 'for a URL with misleading authentication' do + context 'with a URL with misleading authentication' do let(:value) { 'https://spacex.com @h.43z.one' } it 'returns false' do @@ -60,7 +60,7 @@ RSpec.describe Account::Field, type: :model do end end - context 'for a URL' do + context 'with a URL' do let(:value) { 'https://example.com' } it 'returns true' do @@ -68,7 +68,7 @@ RSpec.describe Account::Field, type: :model do end end - context 'for an IDN URL' do + context 'with an IDN URL' do let(:value) { 'https://twitter.com∕dougallj∕status∕1590357240443437057.ê.cc/twitter.html' } it 'returns false' do @@ -76,7 +76,7 @@ RSpec.describe Account::Field, type: :model do end end - context 'for a URL with a non-normalized path' do + context 'with a URL with a non-normalized path' do let(:value) { 'https://github.com/octocatxxxxxxxx/../mastodon' } it 'returns false' do @@ -84,7 +84,7 @@ RSpec.describe Account::Field, type: :model do end end - context 'for text that is not a URL' do + context 'with text that is not a URL' do let(:value) { 'Hello world' } it 'returns false' do @@ -92,7 +92,7 @@ RSpec.describe Account::Field, type: :model do end end - context 'for text that contains a URL' do + context 'with text that contains a URL' do let(:value) { 'Hello https://example.com world' } it 'returns false' do @@ -100,7 +100,7 @@ RSpec.describe Account::Field, type: :model do end end - context 'for text which is blank' do + context 'with text which is blank' do let(:value) { '' } it 'returns false' do @@ -109,10 +109,10 @@ RSpec.describe Account::Field, type: :model do end end - context 'for remote accounts' do + context 'with remote accounts' do let(:local) { false } - context 'for a link' do + context 'with a link' do let(:value) { '<a href="https://www.patreon.com/mastodon" target="_blank" rel="nofollow noopener noreferrer me"><span class="invisible">https://www.</span><span class="">patreon.com/mastodon</span><span class="invisible"></span></a>' } it 'returns true' do @@ -120,7 +120,7 @@ RSpec.describe Account::Field, type: :model do end end - context 'for a link with misleading authentication' do + context 'with a link with misleading authentication' do let(:value) { '<a href="https://google.com @h.43z.one" target="_blank" rel="nofollow noopener noreferrer me"><span class="invisible">https://</span><span class="">google.com</span><span class="invisible"> @h.43z.one</span></a>' } it 'returns false' do @@ -128,7 +128,7 @@ RSpec.describe Account::Field, type: :model do end end - context 'for HTML that has more than just a link' do + context 'with HTML that has more than just a link' do let(:value) { '<a href="https://google.com" target="_blank" rel="nofollow noopener noreferrer me"><span class="invisible">https://</span><span class="">google.com</span><span class="invisible"></span></a> @h.43z.one' } it 'returns false' do @@ -136,7 +136,7 @@ RSpec.describe Account::Field, type: :model do end end - context 'for a link with different visible text' do + context 'with a link with different visible text' do let(:value) { '<a href="https://google.com/bar">https://example.com/foo</a>' } it 'returns false' do @@ -144,7 +144,7 @@ RSpec.describe Account::Field, type: :model do end end - context 'for text that is a URL but is not linked' do + context 'with text that is a URL but is not linked' do let(:value) { 'https://example.com/foo' } it 'returns false' do @@ -152,7 +152,7 @@ RSpec.describe Account::Field, type: :model do end end - context 'for text which is blank' do + context 'with text which is blank' do let(:value) { '' } it 'returns false' do diff --git a/spec/models/account_alias_spec.rb b/spec/models/account_alias_spec.rb index 08c3eaff43..f0cd274ea9 100644 --- a/spec/models/account_alias_spec.rb +++ b/spec/models/account_alias_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe AccountAlias, type: :model do +RSpec.describe AccountAlias do end diff --git a/spec/models/account_conversation_spec.rb b/spec/models/account_conversation_spec.rb index c4e8918ad2..a16aa500cf 100644 --- a/spec/models/account_conversation_spec.rb +++ b/spec/models/account_conversation_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe AccountConversation, type: :model do +RSpec.describe AccountConversation do let!(:alice) { Fabricate(:account, username: 'alice') } let!(:bob) { Fabricate(:account, username: 'bob') } let!(:mark) { Fabricate(:account, username: 'mark') } diff --git a/spec/models/account_deletion_request_spec.rb b/spec/models/account_deletion_request_spec.rb index db332f14cb..8bbfb695d3 100644 --- a/spec/models/account_deletion_request_spec.rb +++ b/spec/models/account_deletion_request_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe AccountDeletionRequest, type: :model do +RSpec.describe AccountDeletionRequest do end diff --git a/spec/models/account_domain_block_spec.rb b/spec/models/account_domain_block_spec.rb index bc46f44ba7..f3246d04c5 100644 --- a/spec/models/account_domain_block_spec.rb +++ b/spec/models/account_domain_block_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe AccountDomainBlock, type: :model do +RSpec.describe AccountDomainBlock do it 'removes blocking cache after creation' do account = Fabricate(:account) Rails.cache.write("exclude_domains_for:#{account.id}", 'a.domain.already.blocked') diff --git a/spec/models/account_migration_spec.rb b/spec/models/account_migration_spec.rb index a91ba5dc56..0d97ea7e77 100644 --- a/spec/models/account_migration_spec.rb +++ b/spec/models/account_migration_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe AccountMigration, type: :model do +RSpec.describe AccountMigration do describe 'validations' do let(:source_account) { Fabricate(:account) } let(:target_acct) { target_account.acct } diff --git a/spec/models/account_moderation_note_spec.rb b/spec/models/account_moderation_note_spec.rb index b7f5701e6a..9d683ca528 100644 --- a/spec/models/account_moderation_note_spec.rb +++ b/spec/models/account_moderation_note_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe AccountModerationNote, type: :model do +RSpec.describe AccountModerationNote do end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 458b2ce52a..bbe35f5793 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Account, type: :model do +RSpec.describe Account do context do subject { Fabricate(:account) } @@ -171,7 +171,7 @@ RSpec.describe Account, type: :model do describe '#possibly_stale?' do let(:account) { Fabricate(:account, last_webfingered_at: last_webfingered_at) } - context 'last_webfingered_at is nil' do + context 'when last_webfingered_at is nil' do let(:last_webfingered_at) { nil } it 'returns true' do @@ -179,7 +179,7 @@ RSpec.describe Account, type: :model do end end - context 'last_webfingered_at is more than 24 hours before' do + context 'when last_webfingered_at is more than 24 hours before' do let(:last_webfingered_at) { 25.hours.ago } it 'returns true' do @@ -187,7 +187,7 @@ RSpec.describe Account, type: :model do end end - context 'last_webfingered_at is less than 24 hours before' do + context 'when last_webfingered_at is less than 24 hours before' do let(:last_webfingered_at) { 23.hours.ago } it 'returns false' do @@ -200,7 +200,7 @@ RSpec.describe Account, type: :model do let(:account) { Fabricate(:account, domain: domain) } let(:acct) { account.acct } - context 'domain is nil' do + context 'when domain is nil' do let(:domain) { nil } it 'returns nil' do @@ -213,7 +213,7 @@ RSpec.describe Account, type: :model do end end - context 'domain is present' do + context 'when domain is present' do let(:domain) { 'example.com' } it 'calls ResolveAccountService#call' do @@ -902,7 +902,7 @@ RSpec.describe Account, type: :model do describe 'recent' do it 'returns a relation of accounts sorted by recent creation' do - matches = 2.times.map { Fabricate(:account) } + matches = Array.new(2) { Fabricate(:account) } expect(Account.where('id > 0').recent).to match_array(matches) end end diff --git a/spec/models/account_statuses_cleanup_policy_spec.rb b/spec/models/account_statuses_cleanup_policy_spec.rb index dcdd97bda0..391b99ff47 100644 --- a/spec/models/account_statuses_cleanup_policy_spec.rb +++ b/spec/models/account_statuses_cleanup_policy_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe AccountStatusesCleanupPolicy, type: :model do +RSpec.describe AccountStatusesCleanupPolicy do let(:account) { Fabricate(:account, username: 'alice', domain: nil) } describe 'validation' do diff --git a/spec/models/admin/account_action_spec.rb b/spec/models/admin/account_action_spec.rb index 9f41b7c8e5..442815c889 100644 --- a/spec/models/admin/account_action_spec.rb +++ b/spec/models/admin/account_action_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::AccountAction, type: :model do +RSpec.describe Admin::AccountAction do let(:account_action) { described_class.new } describe '#save!' do @@ -20,7 +20,7 @@ RSpec.describe Admin::AccountAction, type: :model do ) end - context 'type is "disable"' do + context 'when type is "disable"' do let(:type) { 'disable' } it 'disable user' do @@ -29,7 +29,7 @@ RSpec.describe Admin::AccountAction, type: :model do end end - context 'type is "silence"' do + context 'when type is "silence"' do let(:type) { 'silence' } it 'silences account' do @@ -38,7 +38,7 @@ RSpec.describe Admin::AccountAction, type: :model do end end - context 'type is "suspend"' do + context 'when type is "suspend"' do let(:type) { 'suspend' } it 'suspends account' do @@ -75,7 +75,7 @@ RSpec.describe Admin::AccountAction, type: :model do describe '#report' do subject { account_action.report } - context 'report_id.present?' do + context 'with report_id.present?' do before do account_action.report_id = Fabricate(:report).id end @@ -85,7 +85,7 @@ RSpec.describe Admin::AccountAction, type: :model do end end - context '!report_id.present?' do + context 'with !report_id.present?' do it 'returns nil' do expect(subject).to be_nil end @@ -95,7 +95,7 @@ RSpec.describe Admin::AccountAction, type: :model do describe '#with_report?' do subject { account_action.with_report? } - context '!report.nil?' do + context 'with !report.nil?' do before do account_action.report_id = Fabricate(:report).id end @@ -105,7 +105,7 @@ RSpec.describe Admin::AccountAction, type: :model do end end - context '!(!report.nil?)' do + context 'with !(!report.nil?)' do it 'returns false' do expect(subject).to be false end @@ -115,7 +115,7 @@ RSpec.describe Admin::AccountAction, type: :model do describe '.types_for_account' do subject { described_class.types_for_account(account) } - context 'account.local?' do + context 'when Account.local?' do let(:account) { Fabricate(:account, domain: nil) } it 'returns ["none", "disable", "sensitive", "silence", "suspend"]' do @@ -123,7 +123,7 @@ RSpec.describe Admin::AccountAction, type: :model do end end - context '!account.local?' do + context 'with !account.local?' do let(:account) { Fabricate(:account, domain: 'hoge.com') } it 'returns ["sensitive", "silence", "suspend"]' do diff --git a/spec/models/admin/action_log_spec.rb b/spec/models/admin/action_log_spec.rb index 3495cc5141..1e3649b833 100644 --- a/spec/models/admin/action_log_spec.rb +++ b/spec/models/admin/action_log_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Admin::ActionLog, type: :model do +RSpec.describe Admin::ActionLog do describe '#action' do it 'returns action' do action_log = described_class.new(action: 'hoge') diff --git a/spec/models/announcement_mute_spec.rb b/spec/models/announcement_mute_spec.rb index f4a7a5dc97..1937da3aa7 100644 --- a/spec/models/announcement_mute_spec.rb +++ b/spec/models/announcement_mute_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe AnnouncementMute, type: :model do +RSpec.describe AnnouncementMute do end diff --git a/spec/models/announcement_reaction_spec.rb b/spec/models/announcement_reaction_spec.rb index 38095b0154..43cc0e1489 100644 --- a/spec/models/announcement_reaction_spec.rb +++ b/spec/models/announcement_reaction_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe AnnouncementReaction, type: :model do +RSpec.describe AnnouncementReaction do end diff --git a/spec/models/announcement_spec.rb b/spec/models/announcement_spec.rb index 024fa28880..32d398213c 100644 --- a/spec/models/announcement_spec.rb +++ b/spec/models/announcement_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe Announcement, type: :model do +RSpec.describe Announcement do end diff --git a/spec/models/backup_spec.rb b/spec/models/backup_spec.rb index 239e7aef7b..1303117528 100644 --- a/spec/models/backup_spec.rb +++ b/spec/models/backup_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe Backup, type: :model do +RSpec.describe Backup do end diff --git a/spec/models/block_spec.rb b/spec/models/block_spec.rb index 6e31786d04..de3410fd58 100644 --- a/spec/models/block_spec.rb +++ b/spec/models/block_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Block, type: :model do +RSpec.describe Block do describe 'validations' do it 'is invalid without an account' do block = Fabricate.build(:block, account: nil) diff --git a/spec/models/canonical_email_block_spec.rb b/spec/models/canonical_email_block_spec.rb index 2b3fd6d6a7..0acff82377 100644 --- a/spec/models/canonical_email_block_spec.rb +++ b/spec/models/canonical_email_block_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe CanonicalEmailBlock, type: :model do +RSpec.describe CanonicalEmailBlock do describe '#email=' do let(:target_hash) { '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b' } diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb index 32e08d5f7d..89da661b21 100644 --- a/spec/models/concerns/account_interactions_spec.rb +++ b/spec/models/concerns/account_interactions_spec.rb @@ -13,21 +13,21 @@ describe AccountInteractions do describe '.following_map' do subject { Account.following_map(target_account_ids, account_id) } - context 'account with Follow' do - it 'returns { target_account_id => { reblogs: true } }' do + context 'when Account with Follow' do + it 'returns { target_account_id => true }' do Fabricate(:follow, account: account, target_account: target_account) expect(subject).to eq(target_account_id => { reblogs: true, notify: false, languages: nil }) end end - context 'account with Follow but with reblogs disabled' do + context 'when Account with Follow but with reblogs disabled' do it 'returns { target_account_id => { reblogs: false } }' do Fabricate(:follow, account: account, target_account: target_account, show_reblogs: false) expect(subject).to eq(target_account_id => { reblogs: false, notify: false, languages: nil }) end end - context 'account without Follow' do + context 'when Account without Follow' do it 'returns {}' do expect(subject).to eq({}) end @@ -37,14 +37,14 @@ describe AccountInteractions do describe '.followed_by_map' do subject { Account.followed_by_map(target_account_ids, account_id) } - context 'account with Follow' do + context 'when Account with Follow' do it 'returns { target_account_id => true }' do Fabricate(:follow, account: target_account, target_account: account) expect(subject).to eq(target_account_id => true) end end - context 'account without Follow' do + context 'when Account without Follow' do it 'returns {}' do expect(subject).to eq({}) end @@ -54,14 +54,14 @@ describe AccountInteractions do describe '.blocking_map' do subject { Account.blocking_map(target_account_ids, account_id) } - context 'account with Block' do + context 'when Account with Block' do it 'returns { target_account_id => true }' do Fabricate(:block, account: account, target_account: target_account) expect(subject).to eq(target_account_id => true) end end - context 'account without Block' do + context 'when Account without Block' do it 'returns {}' do expect(subject).to eq({}) end @@ -71,12 +71,12 @@ describe AccountInteractions do describe '.muting_map' do subject { Account.muting_map(target_account_ids, account_id) } - context 'account with Mute' do + context 'when Account with Mute' do before do Fabricate(:mute, target_account: target_account, account: account, hide_notifications: hide) end - context 'if Mute#hide_notifications?' do + context 'when Mute#hide_notifications?' do let(:hide) { true } it 'returns { target_account_id => { notifications: true } }' do @@ -84,7 +84,7 @@ describe AccountInteractions do end end - context 'unless Mute#hide_notifications?' do + context 'when not Mute#hide_notifications?' do let(:hide) { false } it 'returns { target_account_id => { notifications: false } }' do @@ -93,7 +93,7 @@ describe AccountInteractions do end end - context 'account without Mute' do + context 'when Account without Mute' do it 'returns {}' do expect(subject).to eq({}) end @@ -119,8 +119,8 @@ describe AccountInteractions do describe '#mute!' do subject { account.mute!(target_account, notifications: arg_notifications) } - context 'Mute does not exist yet' do - context 'arg :notifications is nil' do + context 'when Mute does not exist yet' do + context 'when arg :notifications is nil' do let(:arg_notifications) { nil } it 'creates Mute, and returns Mute' do @@ -130,7 +130,7 @@ describe AccountInteractions do end end - context 'arg :notifications is false' do + context 'when arg :notifications is false' do let(:arg_notifications) { false } it 'creates Mute, and returns Mute' do @@ -140,7 +140,7 @@ describe AccountInteractions do end end - context 'arg :notifications is true' do + context 'when arg :notifications is true' do let(:arg_notifications) { true } it 'creates Mute, and returns Mute' do @@ -151,7 +151,7 @@ describe AccountInteractions do end end - context 'Mute already exists' do + context 'when Mute already exists' do before do account.mute_relationships << mute end @@ -163,10 +163,10 @@ describe AccountInteractions do hide_notifications: hide_notifications) end - context 'mute.hide_notifications is true' do + context 'when mute.hide_notifications is true' do let(:hide_notifications) { true } - context 'arg :notifications is nil' do + context 'when arg :notifications is nil' do let(:arg_notifications) { nil } it 'returns Mute without updating mute.hide_notifications' do @@ -176,7 +176,7 @@ describe AccountInteractions do end end - context 'arg :notifications is false' do + context 'when arg :notifications is false' do let(:arg_notifications) { false } it 'returns Mute, and updates mute.hide_notifications false' do @@ -186,7 +186,7 @@ describe AccountInteractions do end end - context 'arg :notifications is true' do + context 'when arg :notifications is true' do let(:arg_notifications) { true } it 'returns Mute without updating mute.hide_notifications' do @@ -197,10 +197,10 @@ describe AccountInteractions do end end - context 'mute.hide_notifications is false' do + context 'when mute.hide_notifications is false' do let(:hide_notifications) { false } - context 'arg :notifications is nil' do + context 'when arg :notifications is nil' do let(:arg_notifications) { nil } it 'returns Mute, and updates mute.hide_notifications true' do @@ -210,7 +210,7 @@ describe AccountInteractions do end end - context 'arg :notifications is false' do + context 'when arg :notifications is false' do let(:arg_notifications) { false } it 'returns Mute without updating mute.hide_notifications' do @@ -220,7 +220,7 @@ describe AccountInteractions do end end - context 'arg :notifications is true' do + context 'when arg :notifications is true' do let(:arg_notifications) { true } it 'returns Mute, and updates mute.hide_notifications true' do @@ -260,7 +260,7 @@ describe AccountInteractions do describe '#unfollow!' do subject { account.unfollow!(target_account) } - context 'following target_account' do + context 'when following target_account' do it 'returns destroyed Follow' do account.active_relationships.create(target_account: target_account) expect(subject).to be_a Follow @@ -268,7 +268,7 @@ describe AccountInteractions do end end - context 'not following target_account' do + context 'when not following target_account' do it 'returns nil' do expect(subject).to be_nil end @@ -278,7 +278,7 @@ describe AccountInteractions do describe '#unblock!' do subject { account.unblock!(target_account) } - context 'blocking target_account' do + context 'when blocking target_account' do it 'returns destroyed Block' do account.block_relationships.create(target_account: target_account) expect(subject).to be_a Block @@ -286,7 +286,7 @@ describe AccountInteractions do end end - context 'not blocking target_account' do + context 'when not blocking target_account' do it 'returns nil' do expect(subject).to be_nil end @@ -296,7 +296,7 @@ describe AccountInteractions do describe '#unmute!' do subject { account.unmute!(target_account) } - context 'muting target_account' do + context 'when muting target_account' do it 'returns destroyed Mute' do account.mute_relationships.create(target_account: target_account) expect(subject).to be_a Mute @@ -304,7 +304,7 @@ describe AccountInteractions do end end - context 'not muting target_account' do + context 'when not muting target_account' do it 'returns nil' do expect(subject).to be_nil end @@ -316,7 +316,7 @@ describe AccountInteractions do let(:conversation) { Fabricate(:conversation) } - context 'muting the conversation' do + context 'when muting the conversation' do it 'returns destroyed ConversationMute' do account.conversation_mutes.create(conversation: conversation) expect(subject).to be_a ConversationMute @@ -324,7 +324,7 @@ describe AccountInteractions do end end - context 'not muting the conversation' do + context 'when not muting the conversation' do it 'returns nil' do expect(subject).to be_nil end @@ -336,7 +336,7 @@ describe AccountInteractions do let(:domain) { 'example.com' } - context 'blocking the domain' do + context 'when blocking the domain' do it 'returns destroyed AccountDomainBlock' do account_domain_block = Fabricate(:account_domain_block, domain: domain) account.domain_blocks << account_domain_block @@ -345,7 +345,7 @@ describe AccountInteractions do end end - context 'unblocking the domain' do + context 'when unblocking the domain' do it 'returns nil' do expect(subject).to be_nil end @@ -355,14 +355,14 @@ describe AccountInteractions do describe '#following?' do subject { account.following?(target_account) } - context 'following target_account' do + context 'when following target_account' do it 'returns true' do account.active_relationships.create(target_account: target_account) expect(subject).to be true end end - context 'not following target_account' do + context 'when not following target_account' do it 'returns false' do expect(subject).to be false end @@ -372,14 +372,14 @@ describe AccountInteractions do describe '#followed_by?' do subject { account.followed_by?(target_account) } - context 'followed by target_account' do + context 'when followed by target_account' do it 'returns true' do account.passive_relationships.create(account: target_account) expect(subject).to be true end end - context 'not followed by target_account' do + context 'when not followed by target_account' do it 'returns false' do expect(subject).to be false end @@ -389,14 +389,14 @@ describe AccountInteractions do describe '#blocking?' do subject { account.blocking?(target_account) } - context 'blocking target_account' do + context 'when blocking target_account' do it 'returns true' do account.block_relationships.create(target_account: target_account) expect(subject).to be true end end - context 'not blocking target_account' do + context 'when not blocking target_account' do it 'returns false' do expect(subject).to be false end @@ -408,7 +408,7 @@ describe AccountInteractions do let(:domain) { 'example.com' } - context 'blocking the domain' do + context 'when blocking the domain' do it 'returns true' do account_domain_block = Fabricate(:account_domain_block, domain: domain) account.domain_blocks << account_domain_block @@ -416,7 +416,7 @@ describe AccountInteractions do end end - context 'not blocking the domain' do + context 'when not blocking the domain' do it 'returns false' do expect(subject).to be false end @@ -426,7 +426,7 @@ describe AccountInteractions do describe '#muting?' do subject { account.muting?(target_account) } - context 'muting target_account' do + context 'when muting target_account' do it 'returns true' do mute = Fabricate(:mute, account: account, target_account: target_account) account.mute_relationships << mute @@ -434,7 +434,7 @@ describe AccountInteractions do end end - context 'not muting target_account' do + context 'when not muting target_account' do it 'returns false' do expect(subject).to be false end @@ -446,14 +446,14 @@ describe AccountInteractions do let(:conversation) { Fabricate(:conversation) } - context 'muting the conversation' do + context 'when muting the conversation' do it 'returns true' do account.conversation_mutes.create(conversation: conversation) expect(subject).to be true end end - context 'not muting the conversation' do + context 'when not muting the conversation' do it 'returns false' do expect(subject).to be false end @@ -468,7 +468,7 @@ describe AccountInteractions do account.mute_relationships << mute end - context 'muting notifications of target_account' do + context 'when muting notifications of target_account' do let(:hide) { true } it 'returns true' do @@ -476,7 +476,7 @@ describe AccountInteractions do end end - context 'not muting notifications of target_account' do + context 'when not muting notifications of target_account' do let(:hide) { false } it 'returns false' do @@ -488,14 +488,14 @@ describe AccountInteractions do describe '#requested?' do subject { account.requested?(target_account) } - context 'requested by target_account' do + context 'with requested by target_account' do it 'returns true' do Fabricate(:follow_request, account: account, target_account: target_account) expect(subject).to be true end end - context 'not requested by target_account' do + context 'when not requested by target_account' do it 'returns false' do expect(subject).to be false end @@ -507,7 +507,7 @@ describe AccountInteractions do let(:status) { Fabricate(:status, account: account, favourites: favourites) } - context 'favorited' do + context 'when favorited' do let(:favourites) { [Fabricate(:favourite, account: account)] } it 'returns true' do @@ -515,7 +515,7 @@ describe AccountInteractions do end end - context 'not favorited' do + context 'when not favorited' do let(:favourites) { [] } it 'returns false' do @@ -529,7 +529,7 @@ describe AccountInteractions do let(:status) { Fabricate(:status, account: account, reblogs: reblogs) } - context 'reblogged' do + context 'with reblogged' do let(:reblogs) { [Fabricate(:status, account: account)] } it 'returns true' do @@ -537,7 +537,7 @@ describe AccountInteractions do end end - context 'not reblogged' do + context 'when not reblogged' do let(:reblogs) { [] } it 'returns false' do @@ -551,14 +551,14 @@ describe AccountInteractions do let(:status) { Fabricate(:status, account: account) } - context 'pinned' do + context 'when pinned' do it 'returns true' do Fabricate(:status_pin, account: account, status: status) expect(subject).to be true end end - context 'not pinned' do + context 'when not pinned' do it 'returns false' do expect(subject).to be false end @@ -690,4 +690,32 @@ describe AccountInteractions do end end end + + describe '#lists_for_local_distribution' do + let(:account) { Fabricate(:user, current_sign_in_at: Time.now.utc).account } + let!(:inactive_follower_user) { Fabricate(:user, current_sign_in_at: 5.years.ago) } + let!(:follower_user) { Fabricate(:user, current_sign_in_at: Time.now.utc) } + let!(:follow_request_user) { Fabricate(:user, current_sign_in_at: Time.now.utc) } + + let!(:inactive_follower_list) { Fabricate(:list, account: inactive_follower_user.account) } + let!(:follower_list) { Fabricate(:list, account: follower_user.account) } + let!(:follow_request_list) { Fabricate(:list, account: follow_request_user.account) } + + let!(:self_list) { Fabricate(:list, account: account) } + + before do + inactive_follower_user.account.follow!(account) + follower_user.account.follow!(account) + follow_request_user.account.follow_requests.create!(target_account: account) + + inactive_follower_list.accounts << account + follower_list.accounts << account + follow_request_list.accounts << account + self_list.accounts << account + end + + it 'includes only the list from the active follower and from oneself' do + expect(account.lists_for_local_distribution.to_a).to contain_exactly(follower_list, self_list) + end + end end diff --git a/spec/models/concerns/remotable_spec.rb b/spec/models/concerns/remotable_spec.rb index 9645204276..b2aa56a704 100644 --- a/spec/models/concerns/remotable_spec.rb +++ b/spec/models/concerns/remotable_spec.rb @@ -3,48 +3,47 @@ require 'rails_helper' RSpec.describe Remotable do - class Foo - def initialize - @attrs = {} - end + let(:foo_class) do + Class.new do + def initialize + @attrs = {} + end - def [](arg) - @attrs[arg] - end + def [](arg) + @attrs[arg] + end - def []=(arg1, arg2) - @attrs[arg1] = arg2 - end + def []=(arg1, arg2) + @attrs[arg1] = arg2 + end - def hoge=(arg); end + def hoge=(arg); end - def hoge_file_name; end + def hoge_file_name; end - def hoge_file_name=(arg); end + def hoge_file_name=(arg); end - def has_attribute?(arg); end + def has_attribute?(arg); end - def self.attachment_definitions - { hoge: nil } - end - end - - before do - class Foo - include Remotable - - remotable_attachment :hoge, 1.kilobyte + def self.attachment_definitions + { hoge: nil } + end end end let(:attribute_name) { "#{hoge}_remote_url".to_sym } let(:code) { 200 } let(:file) { 'filename="foo.txt"' } - let(:foo) { Foo.new } + let(:foo) { foo_class.new } let(:headers) { { 'content-disposition' => file } } let(:hoge) { :hoge } let(:url) { 'https://google.com' } + before do + foo_class.include described_class + foo_class.remotable_attachment :hoge, 1.kilobyte + end + it 'defines a method #hoge_remote_url=' do expect(foo).to respond_to(:hoge_remote_url=) end @@ -157,7 +156,7 @@ RSpec.describe Remotable do context 'when the response is successful' do let(:code) { 200 } - context 'and contains Content-Disposition header' do + context 'when contains Content-Disposition header' do let(:file) { 'filename="foo.txt"' } let(:headers) { { 'content-disposition' => file } } diff --git a/spec/models/conversation_mute_spec.rb b/spec/models/conversation_mute_spec.rb index 6439b0ecdf..3d5504a65c 100644 --- a/spec/models/conversation_mute_spec.rb +++ b/spec/models/conversation_mute_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe ConversationMute, type: :model do +RSpec.describe ConversationMute do end diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index 9d58ad0ac6..c1d6659aa7 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Conversation, type: :model do +RSpec.describe Conversation do describe '#local?' do it 'returns true when URI is nil' do expect(Fabricate(:conversation).local?).to be true diff --git a/spec/models/custom_emoji_filter_spec.rb b/spec/models/custom_emoji_filter_spec.rb index 8324a490bc..d8a2bea6b8 100644 --- a/spec/models/custom_emoji_filter_spec.rb +++ b/spec/models/custom_emoji_filter_spec.rb @@ -10,8 +10,8 @@ RSpec.describe CustomEmojiFilter do let!(:custom_emoji_1) { Fabricate(:custom_emoji, domain: 'b') } let!(:custom_emoji_2) { Fabricate(:custom_emoji, domain: nil, shortcode: 'hoge') } - context 'params have values' do - context 'local' do + context 'when params have values' do + context 'when local' do let(:params) { { local: true } } it 'returns ActiveRecord::Relation' do @@ -20,7 +20,7 @@ RSpec.describe CustomEmojiFilter do end end - context 'remote' do + context 'when remote' do let(:params) { { remote: true } } it 'returns ActiveRecord::Relation' do @@ -29,7 +29,7 @@ RSpec.describe CustomEmojiFilter do end end - context 'by_domain' do + context 'with by_domain' do let(:params) { { by_domain: 'a' } } it 'returns ActiveRecord::Relation' do @@ -38,7 +38,7 @@ RSpec.describe CustomEmojiFilter do end end - context 'shortcode' do + context 'when shortcode' do let(:params) { { shortcode: 'hoge' } } it 'returns ActiveRecord::Relation' do @@ -47,7 +47,7 @@ RSpec.describe CustomEmojiFilter do end end - context 'else' do + context 'when some other case' do let(:params) { { else: 'else' } } it 'raises Mastodon::InvalidParameterError' do @@ -58,7 +58,7 @@ RSpec.describe CustomEmojiFilter do end end - context 'params without value' do + context 'when params without value' do let(:params) { { hoge: nil } } it 'returns ActiveRecord::Relation' do diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb index ef5f39aca4..8a6487c321 100644 --- a/spec/models/custom_emoji_spec.rb +++ b/spec/models/custom_emoji_spec.rb @@ -2,13 +2,13 @@ require 'rails_helper' -RSpec.describe CustomEmoji, type: :model do +RSpec.describe CustomEmoji do describe '#search' do subject { described_class.search(search_term) } let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: shortcode) } - context 'shortcode is exact' do + context 'when shortcode is exact' do let(:shortcode) { 'blobpats' } let(:search_term) { 'blobpats' } @@ -17,7 +17,7 @@ RSpec.describe CustomEmoji, type: :model do end end - context 'shortcode is partial' do + context 'when shortcode is partial' do let(:shortcode) { 'blobpats' } let(:search_term) { 'blob' } @@ -32,7 +32,7 @@ RSpec.describe CustomEmoji, type: :model do let(:custom_emoji) { Fabricate(:custom_emoji, domain: domain) } - context 'domain is nil' do + context 'when domain is nil' do let(:domain) { nil } it 'returns true' do @@ -40,7 +40,7 @@ RSpec.describe CustomEmoji, type: :model do end end - context 'domain is present' do + context 'when domain is present' do let(:domain) { 'example.com' } it 'returns false' do diff --git a/spec/models/custom_filter_keyword_spec.rb b/spec/models/custom_filter_keyword_spec.rb index bbc4b9c2ee..09a36fa3ed 100644 --- a/spec/models/custom_filter_keyword_spec.rb +++ b/spec/models/custom_filter_keyword_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe CustomFilterKeyword, type: :model do +RSpec.describe CustomFilterKeyword do end diff --git a/spec/models/custom_filter_spec.rb b/spec/models/custom_filter_spec.rb index d2bc090ab0..f5e9b7ea96 100644 --- a/spec/models/custom_filter_spec.rb +++ b/spec/models/custom_filter_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe CustomFilter, type: :model do +RSpec.describe CustomFilter do end diff --git a/spec/models/device_spec.rb b/spec/models/device_spec.rb index cb214b9cbc..4e7b8f2e20 100644 --- a/spec/models/device_spec.rb +++ b/spec/models/device_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe Device, type: :model do +RSpec.describe Device do end diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb index 9839ee9d4e..f10f470279 100644 --- a/spec/models/domain_block_spec.rb +++ b/spec/models/domain_block_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe DomainBlock, type: :model do +RSpec.describe DomainBlock do describe 'validations' do it 'is invalid without a domain' do domain_block = Fabricate.build(:domain_block, domain: nil) diff --git a/spec/models/email_domain_block_spec.rb b/spec/models/email_domain_block_spec.rb index 3321ffc819..a7232eb6b4 100644 --- a/spec/models/email_domain_block_spec.rb +++ b/spec/models/email_domain_block_spec.rb @@ -2,11 +2,11 @@ require 'rails_helper' -RSpec.describe EmailDomainBlock, type: :model do +RSpec.describe EmailDomainBlock do describe 'block?' do let(:input) { nil } - context 'given an e-mail address' do + context 'when given an e-mail address' do let(:input) { "foo@#{domain}" } context do @@ -33,7 +33,7 @@ RSpec.describe EmailDomainBlock, type: :model do end end - context 'given an array of domains' do + context 'when given an array of domains' do let(:input) { %w(foo.com mail.foo.com) } it 'returns true if the domain is blocked' do diff --git a/spec/models/encrypted_message_spec.rb b/spec/models/encrypted_message_spec.rb index bf7a406ffd..c38142be1e 100644 --- a/spec/models/encrypted_message_spec.rb +++ b/spec/models/encrypted_message_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe EncryptedMessage, type: :model do +RSpec.describe EncryptedMessage do end diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb index 3fb5fc3a5b..a863678a33 100644 --- a/spec/models/export_spec.rb +++ b/spec/models/export_spec.rb @@ -10,7 +10,7 @@ describe Export do describe 'to_csv' do it 'returns a csv of the blocked accounts' do - target_accounts.each(&account.method(:block!)) + target_accounts.each { |target_account| account.block!(target_account) } export = Export.new(account).to_blocked_accounts_csv results = export.strip.split @@ -20,7 +20,7 @@ describe Export do end it 'returns a csv of the muted accounts' do - target_accounts.each(&account.method(:mute!)) + target_accounts.each { |target_account| account.mute!(target_account) } export = Export.new(account).to_muted_accounts_csv results = export.strip.split("\n") @@ -31,7 +31,7 @@ describe Export do end it 'returns a csv of the following accounts' do - target_accounts.each(&account.method(:follow!)) + target_accounts.each { |target_account| account.follow!(target_account) } export = Export.new(account).to_following_accounts_csv results = export.strip.split("\n") @@ -51,17 +51,17 @@ describe Export do describe 'total_follows' do it 'returns the total number of the followed accounts' do - target_accounts.each(&account.method(:follow!)) + target_accounts.each { |target_account| account.follow!(target_account) } expect(Export.new(account.reload).total_follows).to eq 2 end it 'returns the total number of the blocked accounts' do - target_accounts.each(&account.method(:block!)) + target_accounts.each { |target_account| account.block!(target_account) } expect(Export.new(account.reload).total_blocks).to eq 2 end it 'returns the total number of the muted accounts' do - target_accounts.each(&account.method(:mute!)) + target_accounts.each { |target_account| account.mute!(target_account) } expect(Export.new(account.reload).total_mutes).to eq 2 end end diff --git a/spec/models/favourite_spec.rb b/spec/models/favourite_spec.rb index f7e2812a6c..9e69570a01 100644 --- a/spec/models/favourite_spec.rb +++ b/spec/models/favourite_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Favourite, type: :model do +RSpec.describe Favourite do let(:account) { Fabricate(:account) } context 'when status is a reblog' do diff --git a/spec/models/featured_tag_spec.rb b/spec/models/featured_tag_spec.rb index 4bf087c828..58865f94a5 100644 --- a/spec/models/featured_tag_spec.rb +++ b/spec/models/featured_tag_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe FeaturedTag, type: :model do +RSpec.describe FeaturedTag do end diff --git a/spec/models/follow_recommendation_suppression_spec.rb b/spec/models/follow_recommendation_suppression_spec.rb index 4c1d8281b2..d437d170d3 100644 --- a/spec/models/follow_recommendation_suppression_spec.rb +++ b/spec/models/follow_recommendation_suppression_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe FollowRecommendationSuppression, type: :model do +RSpec.describe FollowRecommendationSuppression do end diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb index ff81cd78d9..01faec0e70 100644 --- a/spec/models/follow_request_spec.rb +++ b/spec/models/follow_request_spec.rb @@ -2,15 +2,29 @@ require 'rails_helper' -RSpec.describe FollowRequest, type: :model do +RSpec.describe FollowRequest do describe '#authorize!' do - let(:follow_request) { Fabricate(:follow_request, account: account, target_account: target_account) } - let(:account) { Fabricate(:account) } - let(:target_account) { Fabricate(:account) } + let!(:follow_request) { Fabricate(:follow_request, account: account, target_account: target_account) } + let(:account) { Fabricate(:account) } + let(:target_account) { Fabricate(:account) } + + context 'when the to-be-followed person has been added to a list' do + let!(:list) { Fabricate(:list, account: account) } + + before do + list.accounts << target_account + end + + it 'updates the ListAccount' do + expect { follow_request.authorize! }.to change { [list.list_accounts.first.follow_request_id, list.list_accounts.first.follow_id] }.from([follow_request.id, nil]).to([nil, anything]) + end + end it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do - expect(account).to receive(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri, languages: nil, bypass_limit: true) - expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id) + expect(account).to receive(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri, languages: nil, bypass_limit: true) do + account.active_relationships.create!(target_account: target_account) + end + expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id) expect(follow_request).to receive(:destroy!) follow_request.authorize! end @@ -36,4 +50,22 @@ RSpec.describe FollowRequest, type: :model do expect(follow_request.account.muting_reblogs?(target)).to be true end end + + describe '#reject!' do + let!(:follow_request) { Fabricate(:follow_request, account: account, target_account: target_account) } + let(:account) { Fabricate(:account) } + let(:target_account) { Fabricate(:account) } + + context 'when the to-be-followed person has been added to a list' do + let!(:list) { Fabricate(:list, account: account) } + + before do + list.accounts << target_account + end + + it 'deletes the ListAccount record' do + expect { follow_request.reject! }.to change { list.accounts.count }.from(1).to(0) + end + end + end end diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb index a9a9af88ad..79c0048f9f 100644 --- a/spec/models/follow_spec.rb +++ b/spec/models/follow_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Follow, type: :model do +RSpec.describe Follow do let(:alice) { Fabricate(:account, username: 'alice') } let(:bob) { Fabricate(:account, username: 'bob') } diff --git a/spec/models/form/import_spec.rb b/spec/models/form/import_spec.rb new file mode 100644 index 0000000000..e1fea4205c --- /dev/null +++ b/spec/models/form/import_spec.rb @@ -0,0 +1,281 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Form::Import do + subject { described_class.new(current_account: account, type: import_type, mode: import_mode, data: data) } + + let(:account) { Fabricate(:account) } + let(:data) { fixture_file_upload(import_file) } + let(:import_mode) { 'merge' } + + describe 'validations' do + shared_examples 'incompatible import type' do |type, file| + let(:import_file) { file } + let(:import_type) { type } + + it 'has errors' do + subject.validate + expect(subject.errors[:data]).to include(I18n.t('imports.errors.incompatible_type')) + end + end + + shared_examples 'too many CSV rows' do |type, file, allowed_rows| + let(:import_file) { file } + let(:import_type) { type } + + before do + stub_const 'Form::Import::ROWS_PROCESSING_LIMIT', allowed_rows + end + + it 'has errors' do + subject.validate + expect(subject.errors[:data]).to include(I18n.t('imports.errors.over_rows_processing_limit', count: Form::Import::ROWS_PROCESSING_LIMIT)) + end + end + + shared_examples 'valid import' do |type, file| + let(:import_file) { file } + let(:import_type) { type } + + it 'passes validation' do + expect(subject).to be_valid + end + end + + context 'when the file too large' do + let(:import_type) { 'following' } + let(:import_file) { 'imports.txt' } + + before do + stub_const 'Form::Import::FILE_SIZE_LIMIT', 5 + end + + it 'has errors' do + subject.validate + expect(subject.errors[:data]).to include(I18n.t('imports.errors.too_large')) + end + end + + context 'when the CSV file is malformed CSV' do + let(:import_type) { 'following' } + let(:import_file) { 'boop.ogg' } + + it 'has errors' do + # NOTE: not testing more specific error because we don't know the string to match + expect(subject).to model_have_error_on_field(:data) + end + end + + context 'when importing more follows than allowed' do + let(:import_type) { 'following' } + let(:import_file) { 'imports.txt' } + + before do + allow(FollowLimitValidator).to receive(:limit_for_account).with(account).and_return(1) + end + + it 'has errors' do + subject.validate + expect(subject.errors[:data]).to include(I18n.t('users.follow_limit_reached', limit: 1)) + end + end + + it_behaves_like 'too many CSV rows', 'following', 'imports.txt', 1 + it_behaves_like 'too many CSV rows', 'blocking', 'imports.txt', 1 + it_behaves_like 'too many CSV rows', 'muting', 'imports.txt', 1 + it_behaves_like 'too many CSV rows', 'domain_blocking', 'domain_blocks.csv', 2 + it_behaves_like 'too many CSV rows', 'bookmarks', 'bookmark-imports.txt', 3 + + # Importing list of addresses with no headers into various types + it_behaves_like 'valid import', 'following', 'imports.txt' + it_behaves_like 'valid import', 'blocking', 'imports.txt' + it_behaves_like 'valid import', 'muting', 'imports.txt' + + # Importing domain blocks with headers into expected type + it_behaves_like 'valid import', 'domain_blocking', 'domain_blocks.csv' + + # Importing bookmarks list with no headers into expected type + it_behaves_like 'valid import', 'bookmarks', 'bookmark-imports.txt' + + # Importing followed accounts with headers into various compatible types + it_behaves_like 'valid import', 'following', 'following_accounts.csv' + it_behaves_like 'valid import', 'blocking', 'following_accounts.csv' + it_behaves_like 'valid import', 'muting', 'following_accounts.csv' + + # Importing domain blocks with headers into incompatible types + it_behaves_like 'incompatible import type', 'following', 'domain_blocks.csv' + it_behaves_like 'incompatible import type', 'blocking', 'domain_blocks.csv' + it_behaves_like 'incompatible import type', 'muting', 'domain_blocks.csv' + it_behaves_like 'incompatible import type', 'bookmarks', 'domain_blocks.csv' + + # Importing followed accounts with headers into incompatible types + it_behaves_like 'incompatible import type', 'domain_blocking', 'following_accounts.csv' + it_behaves_like 'incompatible import type', 'bookmarks', 'following_accounts.csv' + end + + describe '#guessed_type' do + shared_examples 'with enough information' do |type, file, original_filename, expected_guess| + let(:import_file) { file } + let(:import_type) { type } + + before do + allow(data).to receive(:original_filename).and_return(original_filename) + end + + it 'guesses the expected type' do + expect(subject.guessed_type).to eq expected_guess + end + end + + context 'when the headers are enough to disambiguate' do + it_behaves_like 'with enough information', 'following', 'following_accounts.csv', 'import.csv', :following + it_behaves_like 'with enough information', 'blocking', 'following_accounts.csv', 'import.csv', :following + it_behaves_like 'with enough information', 'muting', 'following_accounts.csv', 'import.csv', :following + + it_behaves_like 'with enough information', 'following', 'muted_accounts.csv', 'imports.csv', :muting + it_behaves_like 'with enough information', 'blocking', 'muted_accounts.csv', 'imports.csv', :muting + it_behaves_like 'with enough information', 'muting', 'muted_accounts.csv', 'imports.csv', :muting + end + + context 'when the file name is enough to disambiguate' do + it_behaves_like 'with enough information', 'following', 'imports.txt', 'following_accounts.csv', :following + it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'following_accounts.csv', :following + it_behaves_like 'with enough information', 'muting', 'imports.txt', 'following_accounts.csv', :following + + it_behaves_like 'with enough information', 'following', 'imports.txt', 'follows.csv', :following + it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'follows.csv', :following + it_behaves_like 'with enough information', 'muting', 'imports.txt', 'follows.csv', :following + + it_behaves_like 'with enough information', 'following', 'imports.txt', 'blocked_accounts.csv', :blocking + it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'blocked_accounts.csv', :blocking + it_behaves_like 'with enough information', 'muting', 'imports.txt', 'blocked_accounts.csv', :blocking + + it_behaves_like 'with enough information', 'following', 'imports.txt', 'blocks.csv', :blocking + it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'blocks.csv', :blocking + it_behaves_like 'with enough information', 'muting', 'imports.txt', 'blocks.csv', :blocking + + it_behaves_like 'with enough information', 'following', 'imports.txt', 'muted_accounts.csv', :muting + it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'muted_accounts.csv', :muting + it_behaves_like 'with enough information', 'muting', 'imports.txt', 'muted_accounts.csv', :muting + + it_behaves_like 'with enough information', 'following', 'imports.txt', 'mutes.csv', :muting + it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'mutes.csv', :muting + it_behaves_like 'with enough information', 'muting', 'imports.txt', 'mutes.csv', :muting + end + end + + describe '#likely_mismatched?' do + shared_examples 'with matching types' do |type, file, original_filename = nil| + let(:import_file) { file } + let(:import_type) { type } + + before do + allow(data).to receive(:original_filename).and_return(original_filename) if original_filename.present? + end + + it 'returns false' do + expect(subject.likely_mismatched?).to be false + end + end + + shared_examples 'with mismatching types' do |type, file, original_filename = nil| + let(:import_file) { file } + let(:import_type) { type } + + before do + allow(data).to receive(:original_filename).and_return(original_filename) if original_filename.present? + end + + it 'returns true' do + expect(subject.likely_mismatched?).to be true + end + end + + it_behaves_like 'with matching types', 'following', 'following_accounts.csv' + it_behaves_like 'with matching types', 'following', 'following_accounts.csv', 'imports.txt' + it_behaves_like 'with matching types', 'following', 'imports.txt' + it_behaves_like 'with matching types', 'blocking', 'imports.txt', 'blocks.csv' + it_behaves_like 'with matching types', 'blocking', 'imports.txt' + it_behaves_like 'with matching types', 'muting', 'muted_accounts.csv' + it_behaves_like 'with matching types', 'muting', 'muted_accounts.csv', 'imports.txt' + it_behaves_like 'with matching types', 'muting', 'imports.txt' + it_behaves_like 'with matching types', 'domain_blocking', 'domain_blocks.csv' + it_behaves_like 'with matching types', 'domain_blocking', 'domain_blocks.csv', 'imports.txt' + it_behaves_like 'with matching types', 'bookmarks', 'bookmark-imports.txt' + it_behaves_like 'with matching types', 'bookmarks', 'bookmark-imports.txt', 'imports.txt' + + it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'blocks.csv' + it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'blocked_accounts.csv' + it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'mutes.csv' + it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'muted_accounts.csv' + it_behaves_like 'with mismatching types', 'following', 'muted_accounts.csv' + it_behaves_like 'with mismatching types', 'following', 'muted_accounts.csv', 'imports.txt' + it_behaves_like 'with mismatching types', 'blocking', 'following_accounts.csv' + it_behaves_like 'with mismatching types', 'blocking', 'following_accounts.csv', 'imports.txt' + it_behaves_like 'with mismatching types', 'blocking', 'muted_accounts.csv' + it_behaves_like 'with mismatching types', 'blocking', 'muted_accounts.csv', 'imports.txt' + it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'follows.csv' + it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'following_accounts.csv' + it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'mutes.csv' + it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'muted_accounts.csv' + it_behaves_like 'with mismatching types', 'muting', 'following_accounts.csv' + it_behaves_like 'with mismatching types', 'muting', 'following_accounts.csv', 'imports.txt' + it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'follows.csv' + it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'following_accounts.csv' + it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'blocks.csv' + it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'blocked_accounts.csv' + end + + describe 'save' do + shared_examples 'on successful import' do |type, mode, file, expected_rows| + let(:import_type) { type } + let(:import_file) { file } + let(:import_mode) { mode } + + before do + subject.save + end + + it 'creates the expected rows' do + expect(account.bulk_imports.first.rows.pluck(:data)).to match_array(expected_rows) + end + + it 'creates a BulkImport with expected attributes' do + bulk_import = account.bulk_imports.first + expect(bulk_import).to_not be_nil + expect(bulk_import.type.to_sym).to eq subject.type.to_sym + expect(bulk_import.original_filename).to eq subject.data.original_filename + expect(bulk_import.likely_mismatched?).to eq subject.likely_mismatched? + expect(bulk_import.overwrite?).to eq !!subject.overwrite # rubocop:disable Style/DoubleNegation + expect(bulk_import.processed_items).to eq 0 + expect(bulk_import.imported_items).to eq 0 + expect(bulk_import.total_items).to eq bulk_import.rows.count + expect(bulk_import.unconfirmed?).to be true + end + end + + it_behaves_like 'on successful import', 'following', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like 'on successful import', 'following', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like 'on successful import', 'blocking', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like 'on successful import', 'blocking', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like 'on successful import', 'muting', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like 'on successful import', 'domain_blocking', 'merge', 'domain_blocks.csv', (%w(bad.domain worse.domain reject.media).map { |domain| { 'domain' => domain } }) + it_behaves_like 'on successful import', 'bookmarks', 'merge', 'bookmark-imports.txt', (%w(https://example.com/statuses/1312 https://local.com/users/foo/statuses/42 https://unknown-remote.com/users/bar/statuses/1 https://example.com/statuses/direct).map { |uri| { 'uri' => uri } }) + + it_behaves_like 'on successful import', 'following', 'merge', 'following_accounts.csv', [ + { 'acct' => 'user@example.com', 'show_reblogs' => true, 'notify' => false, 'languages' => nil }, + { 'acct' => 'user@test.com', 'show_reblogs' => true, 'notify' => true, 'languages' => ['en', 'fr'] }, + ] + + it_behaves_like 'on successful import', 'muting', 'merge', 'muted_accounts.csv', [ + { 'acct' => 'user@example.com', 'hide_notifications' => true }, + { 'acct' => 'user@test.com', 'hide_notifications' => false }, + ] + + # Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users + # + # https://github.com/mastodon/mastodon/issues/20571 + it_behaves_like 'on successful import', 'following', 'merge', 'utf8-followers.txt', [{ 'acct' => 'nare@թութ.հայ' }] + end +end diff --git a/spec/models/home_feed_spec.rb b/spec/models/home_feed_spec.rb index d7034f3f0b..bd649d8269 100644 --- a/spec/models/home_feed_spec.rb +++ b/spec/models/home_feed_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe HomeFeed, type: :model do +RSpec.describe HomeFeed do subject { described_class.new(account) } let(:account) { Fabricate(:account) } diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb index 6eab5a2e18..59155781c7 100644 --- a/spec/models/identity_spec.rb +++ b/spec/models/identity_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Identity, type: :model do +RSpec.describe Identity do describe '.find_for_oauth' do let(:auth) { Fabricate(:identity, user: Fabricate(:user)) } diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb index 1c84744138..1dae40a739 100644 --- a/spec/models/import_spec.rb +++ b/spec/models/import_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Import, type: :model do +RSpec.describe Import do let(:account) { Fabricate(:account) } let(:type) { 'following' } let(:data) { attachment_fixture('imports.txt') } @@ -22,20 +22,5 @@ RSpec.describe Import, type: :model do import = Import.create(account: account, type: type) expect(import).to model_have_error_on_field(:data) end - - it 'is invalid with malformed data' do - import = Import.create(account: account, type: type, data: StringIO.new('\"test')) - expect(import).to model_have_error_on_field(:data) - end - - it 'is invalid with too many rows in data' do - import = Import.create(account: account, type: type, data: StringIO.new("foo@bar.com\n" * (ImportService::ROWS_PROCESSING_LIMIT + 10))) - expect(import).to model_have_error_on_field(:data) - end - - it 'is invalid when there are more rows when following limit' do - import = Import.create(account: account, type: type, data: StringIO.new("foo@bar.com\n" * (FollowLimitValidator.limit_for_account(account) + 10))) - expect(import).to model_have_error_on_field(:data) - end end end diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index dac4b6431b..4ad589f2c7 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Invite, type: :model do +RSpec.describe Invite do describe '#valid_for_use?' do it 'returns true when there are no limitations' do invite = Fabricate(:invite, max_uses: nil, expires_at: nil) diff --git a/spec/models/list_account_spec.rb b/spec/models/list_account_spec.rb index 8312defaca..c9853d4a04 100644 --- a/spec/models/list_account_spec.rb +++ b/spec/models/list_account_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe ListAccount, type: :model do +RSpec.describe ListAccount do end diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index 8167f8a7ec..621ad3968d 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe List, type: :model do +RSpec.describe List do end diff --git a/spec/models/login_activity_spec.rb b/spec/models/login_activity_spec.rb index 1c3111a20d..2214d3ef39 100644 --- a/spec/models/login_activity_spec.rb +++ b/spec/models/login_activity_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe LoginActivity, type: :model do +RSpec.describe LoginActivity do end diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index 63edfc1524..becc748244 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -2,13 +2,13 @@ require 'rails_helper' -RSpec.describe MediaAttachment, type: :model do +RSpec.describe MediaAttachment do describe 'local?' do subject { media_attachment.local? } let(:media_attachment) { Fabricate(:media_attachment, remote_url: remote_url) } - context 'remote_url is blank' do + context 'when remote_url is blank' do let(:remote_url) { '' } it 'returns true' do @@ -16,7 +16,7 @@ RSpec.describe MediaAttachment, type: :model do end end - context 'remote_url is present' do + context 'when remote_url is present' do let(:remote_url) { 'remote_url' } it 'returns false' do @@ -30,10 +30,10 @@ RSpec.describe MediaAttachment, type: :model do let(:media_attachment) { Fabricate(:media_attachment, remote_url: remote_url, file: file) } - context 'file is blank' do + context 'when file is blank' do let(:file) { nil } - context 'remote_url is present' do + context 'when remote_url is present' do let(:remote_url) { 'remote_url' } it 'returns true' do @@ -42,10 +42,10 @@ RSpec.describe MediaAttachment, type: :model do end end - context 'file is present' do + context 'when file is present' do let(:file) { attachment_fixture('avatar.gif') } - context 'remote_url is blank' do + context 'when remote_url is blank' do let(:remote_url) { '' } it 'returns false' do @@ -53,7 +53,7 @@ RSpec.describe MediaAttachment, type: :model do end end - context 'remote_url is present' do + context 'when remote_url is present' do let(:remote_url) { 'remote_url' } it 'returns true' do diff --git a/spec/models/mention_spec.rb b/spec/models/mention_spec.rb index 044bb80cf6..b241049a54 100644 --- a/spec/models/mention_spec.rb +++ b/spec/models/mention_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Mention, type: :model do +RSpec.describe Mention do describe 'validations' do it 'is invalid without an account' do mention = Fabricate.build(:mention, account: nil) diff --git a/spec/models/mute_spec.rb b/spec/models/mute_spec.rb index 48b5a37ab9..050083d0f8 100644 --- a/spec/models/mute_spec.rb +++ b/spec/models/mute_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe Mute, type: :model do +RSpec.describe Mute do end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 64527e3d77..795491546c 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Notification, type: :model do +RSpec.describe Notification do describe '#target_status' do let(:notification) { Fabricate(:notification, activity: activity) } let(:status) { Fabricate(:status) } @@ -10,7 +10,7 @@ RSpec.describe Notification, type: :model do let(:favourite) { Fabricate(:favourite, status: status) } let(:mention) { Fabricate(:mention, status: status) } - context 'activity is reblog' do + context 'when Activity is reblog' do let(:activity) { reblog } it 'returns status' do @@ -18,7 +18,7 @@ RSpec.describe Notification, type: :model do end end - context 'activity is favourite' do + context 'when Activity is favourite' do let(:type) { :favourite } let(:activity) { favourite } @@ -27,7 +27,7 @@ RSpec.describe Notification, type: :model do end end - context 'activity is mention' do + context 'when Activity is mention' do let(:activity) { mention } it 'returns status' do @@ -66,7 +66,7 @@ RSpec.describe Notification, type: :model do end end - context 'notifications are empty' do + context 'when notifications are empty' do let(:notifications) { [] } it 'returns []' do @@ -74,7 +74,7 @@ RSpec.describe Notification, type: :model do end end - context 'notifications are present' do + context 'when notifications are present' do before do notifications.each(&:reload) end diff --git a/spec/models/poll_vote_spec.rb b/spec/models/poll_vote_spec.rb index 6886a82aa8..b017ea5279 100644 --- a/spec/models/poll_vote_spec.rb +++ b/spec/models/poll_vote_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe PollVote, type: :model do +RSpec.describe PollVote do describe '#object_type' do let(:poll_vote) { Fabricate.build(:poll_vote) } diff --git a/spec/models/preview_card_spec.rb b/spec/models/preview_card_spec.rb index 1858644c91..7d687d16ff 100644 --- a/spec/models/preview_card_spec.rb +++ b/spec/models/preview_card_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe PreviewCard, type: :model do +RSpec.describe PreviewCard do end diff --git a/spec/models/preview_card_trend_spec.rb b/spec/models/preview_card_trend_spec.rb index 97ad05e754..a31bf71cc0 100644 --- a/spec/models/preview_card_trend_spec.rb +++ b/spec/models/preview_card_trend_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe PreviewCardTrend, type: :model do +RSpec.describe PreviewCardTrend do end diff --git a/spec/models/public_feed_spec.rb b/spec/models/public_feed_spec.rb index d31aba084d..fbbdf62584 100644 --- a/spec/models/public_feed_spec.rb +++ b/spec/models/public_feed_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe PublicFeed, type: :model do +RSpec.describe PublicFeed do let(:account) { Fabricate(:account) } describe '#get' do diff --git a/spec/models/relay_spec.rb b/spec/models/relay_spec.rb index 86c1762c15..7ed49e7334 100644 --- a/spec/models/relay_spec.rb +++ b/spec/models/relay_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe Relay, type: :model do +RSpec.describe Relay do end diff --git a/spec/models/remote_follow_spec.rb b/spec/models/remote_follow_spec.rb index ea36b00769..81c726a40b 100644 --- a/spec/models/remote_follow_spec.rb +++ b/spec/models/remote_follow_spec.rb @@ -13,7 +13,7 @@ RSpec.describe RemoteFollow do describe '.initialize' do subject { remote_follow.acct } - context 'attrs with acct' do + context 'when attrs with acct' do let(:attrs) { { acct: 'gargron@quitter.no' } } it 'returns acct' do @@ -21,7 +21,7 @@ RSpec.describe RemoteFollow do end end - context 'attrs without acct' do + context 'when attrs without acct' do let(:attrs) { {} } it do @@ -33,7 +33,7 @@ RSpec.describe RemoteFollow do describe '#valid?' do subject { remote_follow.valid? } - context 'attrs with acct' do + context 'when attrs with acct' do let(:attrs) { { acct: 'gargron@quitter.no' } } it do @@ -41,7 +41,7 @@ RSpec.describe RemoteFollow do end end - context 'attrs without acct' do + context 'when attrs without acct' do let(:attrs) { {} } it do diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index 20a048c334..b006f60bb6 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -89,13 +89,13 @@ describe Report do let(:report) { Fabricate(:report, action_taken_at: action_taken) } - context 'if action is taken' do + context 'when action is taken' do let(:action_taken) { Time.now.utc } it { is_expected.to be false } end - context 'if action not is taken' do + context 'when action not is taken' do let(:action_taken) { nil } it { is_expected.to be true } diff --git a/spec/models/scheduled_status_spec.rb b/spec/models/scheduled_status_spec.rb index 294fa9f36c..286c17e696 100644 --- a/spec/models/scheduled_status_spec.rb +++ b/spec/models/scheduled_status_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe ScheduledStatus, type: :model do +RSpec.describe ScheduledStatus do end diff --git a/spec/models/session_activation_spec.rb b/spec/models/session_activation_spec.rb index 375199d575..51c6aa5cb0 100644 --- a/spec/models/session_activation_spec.rb +++ b/spec/models/session_activation_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe SessionActivation, type: :model do +RSpec.describe SessionActivation do describe '#detection' do let(:session_activation) { Fabricate(:session_activation, user_agent: 'Chrome/62.0.3202.89') } @@ -40,7 +40,7 @@ RSpec.describe SessionActivation, type: :model do describe '.active?' do subject { described_class.active?(id) } - context 'id is absent' do + context 'when id is absent' do let(:id) { nil } it 'returns nil' do @@ -48,17 +48,17 @@ RSpec.describe SessionActivation, type: :model do end end - context 'id is present' do + context 'when id is present' do let(:id) { '1' } let!(:session_activation) { Fabricate(:session_activation, session_id: id) } - context 'id exists as session_id' do + context 'when id exists as session_id' do it 'returns true' do expect(subject).to be true end end - context 'id does not exist as session_id' do + context 'when id does not exist as session_id' do before do session_activation.update!(session_id: '2') end @@ -85,7 +85,7 @@ RSpec.describe SessionActivation, type: :model do end describe '.deactivate' do - context 'id is absent' do + context 'when id is absent' do let(:id) { nil } it 'returns nil' do @@ -93,7 +93,7 @@ RSpec.describe SessionActivation, type: :model do end end - context 'id exists' do + context 'when id exists' do let(:id) { '1' } it 'calls where.destroy_all' do diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb index 826a13878f..accce10f86 100644 --- a/spec/models/setting_spec.rb +++ b/spec/models/setting_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Setting, type: :model do +RSpec.describe Setting do describe '#to_param' do let(:setting) { Fabricate(:setting, var: var) } let(:var) { 'var' } @@ -19,7 +19,7 @@ RSpec.describe Setting, type: :model do let(:key) { 'key' } - context 'rails_initialized? is falsey' do + context 'when rails_initialized? is falsey' do let(:rails_initialized) { false } it 'calls RailsSettings::Base#[]' do @@ -28,7 +28,7 @@ RSpec.describe Setting, type: :model do end end - context 'rails_initialized? is truthy' do + context 'when rails_initialized? is truthy' do before do allow(RailsSettings::Base).to receive(:cache_key).with(key, nil).and_return(cache_key) end @@ -42,7 +42,7 @@ RSpec.describe Setting, type: :model do described_class[key] end - context 'Rails.cache does not exists' do + context 'when Rails.cache does not exists' do before do allow(RailsSettings::Settings).to receive(:object).with(key).and_return(object) allow(described_class).to receive(:default_settings).and_return(default_settings) @@ -60,11 +60,11 @@ RSpec.describe Setting, type: :model do described_class[key] end - context 'RailsSettings::Settings.object returns truthy' do + context 'when RailsSettings::Settings.object returns truthy' do let(:object) { db_val } let(:db_val) { double(value: 'db_val') } - context 'default_value is a Hash' do + context 'when default_value is a Hash' do let(:default_value) { { default_value: 'default_value' } } it 'calls default_value.with_indifferent_access.merge!' do @@ -75,7 +75,7 @@ RSpec.describe Setting, type: :model do end end - context 'default_value is not a Hash' do + context 'when default_value is not a Hash' do let(:default_value) { 'default_value' } it 'returns db_val.value' do @@ -84,7 +84,7 @@ RSpec.describe Setting, type: :model do end end - context 'RailsSettings::Settings.object returns falsey' do + context 'when RailsSettings::Settings.object returns falsey' do let(:object) { nil } it 'returns default_settings[key]' do @@ -93,7 +93,7 @@ RSpec.describe Setting, type: :model do end end - context 'Rails.cache exists' do + context 'when Rails.cache exists' do before do Rails.cache.write(cache_key, cache_value) end @@ -130,7 +130,7 @@ RSpec.describe Setting, type: :model do expect(described_class.all_as_records).to be_a Hash end - context 'records includes Setting with var as the key' do + context 'when records includes Setting with var as the key' do let(:records) { [original_setting] } it 'includes the original Setting' do @@ -139,10 +139,10 @@ RSpec.describe Setting, type: :model do end end - context 'records includes nothing' do + context 'when records includes nothing' do let(:records) { [] } - context 'default_value is not a Hash' do + context 'when default_value is not a Hash' do it 'includes Setting with value of default_value' do setting = described_class.all_as_records[key] @@ -152,7 +152,7 @@ RSpec.describe Setting, type: :model do end end - context 'default_value is a Hash' do + context 'when default_value is a Hash' do let(:default_value) { { 'foo' => 'fuga' } } it 'returns {}' do @@ -169,7 +169,7 @@ RSpec.describe Setting, type: :model do allow(RailsSettings::Default).to receive(:enabled?).and_return(enabled) end - context 'RailsSettings::Default.enabled? is false' do + context 'when RailsSettings::Default.enabled? is false' do let(:enabled) { false } it 'returns {}' do @@ -177,7 +177,7 @@ RSpec.describe Setting, type: :model do end end - context 'RailsSettings::Settings.enabled? is true' do + context 'when RailsSettings::Settings.enabled? is true' do let(:enabled) { true } it 'returns instance of RailsSettings::Default' do diff --git a/spec/models/site_upload_spec.rb b/spec/models/site_upload_spec.rb index f7ea069213..d4a9293115 100644 --- a/spec/models/site_upload_spec.rb +++ b/spec/models/site_upload_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe SiteUpload, type: :model do +RSpec.describe SiteUpload do describe '#cache_key' do let(:site_upload) { SiteUpload.new(var: 'var') } diff --git a/spec/models/status_pin_spec.rb b/spec/models/status_pin_spec.rb index c4ebf96da9..52ce0847c4 100644 --- a/spec/models/status_pin_spec.rb +++ b/spec/models/status_pin_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe StatusPin, type: :model do +RSpec.describe StatusPin do describe 'validations' do it 'allows pins of own statuses' do account = Fabricate(:account) diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 8fa1db1917..3b612ded14 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Status, type: :model do +RSpec.describe Status do subject { Fabricate(:status, account: alice) } let(:alice) { Fabricate(:account, username: 'alice') } @@ -49,22 +49,22 @@ RSpec.describe Status, type: :model do end describe '#verb' do - context 'if destroyed?' do + context 'when destroyed?' do it 'returns :delete' do subject.destroy! expect(subject.verb).to be :delete end end - context 'unless destroyed?' do - context 'if reblog?' do + context 'when not destroyed?' do + context 'when reblog?' do it 'returns :share' do subject.reblog = other expect(subject.verb).to be :share end end - context 'unless reblog?' do + context 'when not reblog?' do it 'returns :post' do subject.reblog = nil expect(subject.verb).to be :post @@ -85,28 +85,28 @@ RSpec.describe Status, type: :model do end describe '#hidden?' do - context 'if private_visibility?' do + context 'when private_visibility?' do it 'returns true' do subject.visibility = :private expect(subject.hidden?).to be true end end - context 'if direct_visibility?' do + context 'when direct_visibility?' do it 'returns true' do subject.visibility = :direct expect(subject.hidden?).to be true end end - context 'if public_visibility?' do + context 'when public_visibility?' do it 'returns false' do subject.visibility = :public expect(subject.hidden?).to be false end end - context 'if unlisted_visibility?' do + context 'when unlisted_visibility?' do it 'returns false' do subject.visibility = :unlisted expect(subject.hidden?).to be false @@ -216,7 +216,7 @@ RSpec.describe Status, type: :model do subject.text = "A toot #{subject.local_only_emoji}" end - context 'if the status originates from this instance' do + context 'when the status originates from this instance' do before do subject.account = local_account end @@ -228,7 +228,7 @@ RSpec.describe Status, type: :model do end end - context 'if the status is remote' do + context 'when the status is remote' do before do subject.account = remote_account end diff --git a/spec/models/status_stat_spec.rb b/spec/models/status_stat_spec.rb index 749ca097d6..9679c836a7 100644 --- a/spec/models/status_stat_spec.rb +++ b/spec/models/status_stat_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe StatusStat, type: :model do +RSpec.describe StatusStat do end diff --git a/spec/models/status_trend_spec.rb b/spec/models/status_trend_spec.rb index 9678b838a7..dbb3d4bb3f 100644 --- a/spec/models/status_trend_spec.rb +++ b/spec/models/status_trend_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe StatusTrend, type: :model do +RSpec.describe StatusTrend do end diff --git a/spec/models/system_key_spec.rb b/spec/models/system_key_spec.rb index a4e8b77844..5bd630aaa5 100644 --- a/spec/models/system_key_spec.rb +++ b/spec/models/system_key_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe SystemKey, type: :model do +RSpec.describe SystemKey do end diff --git a/spec/models/tag_follow_spec.rb b/spec/models/tag_follow_spec.rb index 88409bb28a..240147ecc8 100644 --- a/spec/models/tag_follow_spec.rb +++ b/spec/models/tag_follow_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe TagFollow, type: :model do +RSpec.describe TagFollow do end diff --git a/spec/models/unavailable_domain_spec.rb b/spec/models/unavailable_domain_spec.rb index 5469ff6939..b868779f2b 100644 --- a/spec/models/unavailable_domain_spec.rb +++ b/spec/models/unavailable_domain_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe UnavailableDomain, type: :model do +RSpec.describe UnavailableDomain do end diff --git a/spec/models/user_invite_request_spec.rb b/spec/models/user_invite_request_spec.rb index 95e1284399..ee0efbb431 100644 --- a/spec/models/user_invite_request_spec.rb +++ b/spec/models/user_invite_request_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe UserInviteRequest, type: :model do +RSpec.describe UserInviteRequest do end diff --git a/spec/models/user_role_spec.rb b/spec/models/user_role_spec.rb index 97456c1060..16bb4358e6 100644 --- a/spec/models/user_role_spec.rb +++ b/spec/models/user_role_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe UserRole, type: :model do +RSpec.describe UserRole do subject { described_class.create(name: 'Foo', position: 1) } describe '#can?' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b7754e9265..ae46f0ae45 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' require 'devise_two_factor/spec_helpers' -RSpec.describe User, type: :model do +RSpec.describe User do let(:password) { 'abcd1234' } let(:account) { Fabricate(:account, username: 'alice') } diff --git a/spec/models/web/push_subscription_spec.rb b/spec/models/web/push_subscription_spec.rb index e925e4c4cd..3c2cd3bac1 100644 --- a/spec/models/web/push_subscription_spec.rb +++ b/spec/models/web/push_subscription_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Web::PushSubscription, type: :model do +RSpec.describe Web::PushSubscription do subject { described_class.new(data: data) } let(:account) { Fabricate(:account) } @@ -56,7 +56,7 @@ RSpec.describe Web::PushSubscription, type: :model do context 'when policy is followed' do let(:policy) { 'followed' } - context 'and notification is from someone you follow' do + context 'when notification is from someone you follow' do before do account.follow!(notification.from_account) end @@ -66,7 +66,7 @@ RSpec.describe Web::PushSubscription, type: :model do end end - context 'and notification is not from someone you follow' do + context 'when notification is not from someone you follow' do it 'returns false' do expect(subject.pushable?(notification)).to be false end @@ -76,7 +76,7 @@ RSpec.describe Web::PushSubscription, type: :model do context 'when policy is follower' do let(:policy) { 'follower' } - context 'and notification is from someone who follows you' do + context 'when notification is from someone who follows you' do before do notification.from_account.follow!(account) end @@ -86,7 +86,7 @@ RSpec.describe Web::PushSubscription, type: :model do end end - context 'and notification is not from someone who follows you' do + context 'when notification is not from someone who follows you' do it 'returns false' do expect(subject.pushable?(notification)).to be false end diff --git a/spec/models/web/setting_spec.rb b/spec/models/web/setting_spec.rb index b7ff3c8684..3182c67217 100644 --- a/spec/models/web/setting_spec.rb +++ b/spec/models/web/setting_spec.rb @@ -2,5 +2,5 @@ require 'rails_helper' -RSpec.describe Web::Setting, type: :model do +RSpec.describe Web::Setting do end diff --git a/spec/models/webauthn_credentials_spec.rb b/spec/models/webauthn_credentials_spec.rb index 1a2a2f9099..4579ebb82e 100644 --- a/spec/models/webauthn_credentials_spec.rb +++ b/spec/models/webauthn_credentials_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe WebauthnCredential, type: :model do +RSpec.describe WebauthnCredential do describe 'validations' do it 'is invalid without an external id' do webauthn_credential = Fabricate.build(:webauthn_credential, external_id: nil) diff --git a/spec/models/webhook_spec.rb b/spec/models/webhook_spec.rb index fcf3dd14ff..715dd7574f 100644 --- a/spec/models/webhook_spec.rb +++ b/spec/models/webhook_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Webhook, type: :model do +RSpec.describe Webhook do let(:webhook) { Fabricate(:webhook) } describe '#rotate_secret!' do diff --git a/spec/policies/account_moderation_note_policy_spec.rb b/spec/policies/account_moderation_note_policy_spec.rb index 8467473465..03d18250b2 100644 --- a/spec/policies/account_moderation_note_policy_spec.rb +++ b/spec/policies/account_moderation_note_policy_spec.rb @@ -9,13 +9,13 @@ RSpec.describe AccountModerationNotePolicy do let(:john) { Fabricate(:account) } permissions :create? do - context 'staff' do + context 'when staff' do it 'grants to create' do expect(subject).to permit(admin, AccountModerationNotePolicy) end end - context 'not staff' do + context 'when not staff' do it 'denies to create' do expect(subject).to_not permit(john, AccountModerationNotePolicy) end @@ -29,19 +29,19 @@ RSpec.describe AccountModerationNotePolicy do target_account: Fabricate(:account)) end - context 'admin' do + context 'when admin' do it 'grants to destroy' do expect(subject).to permit(admin, account_moderation_note) end end - context 'owner' do + context 'when owner' do it 'grants to destroy' do expect(subject).to permit(john, account_moderation_note) end end - context 'neither admin nor owner' do + context 'when neither admin nor owner' do let(:kevin) { Fabricate(:account) } it 'denies to destroy' do diff --git a/spec/policies/account_policy_spec.rb b/spec/policies/account_policy_spec.rb index d961532332..9f4e94a6c8 100644 --- a/spec/policies/account_policy_spec.rb +++ b/spec/policies/account_policy_spec.rb @@ -10,13 +10,13 @@ RSpec.describe AccountPolicy do let(:alice) { Fabricate(:account) } permissions :index? do - context 'staff' do + context 'when staff' do it 'permits' do expect(subject).to permit(admin) end end - context 'not staff' do + context 'when not staff' do it 'denies' do expect(subject).to_not permit(john) end @@ -24,13 +24,13 @@ RSpec.describe AccountPolicy do end permissions :show?, :unsilence?, :unsensitive?, :remove_avatar?, :remove_header? do - context 'staff' do + context 'when staff' do it 'permits' do expect(subject).to permit(admin, alice) end end - context 'not staff' do + context 'when not staff' do it 'denies' do expect(subject).to_not permit(john, alice) end @@ -42,13 +42,13 @@ RSpec.describe AccountPolicy do alice.suspend! end - context 'staff' do + context 'when staff' do it 'permits' do expect(subject).to permit(admin, alice) end end - context 'not staff' do + context 'when not staff' do it 'denies' do expect(subject).to_not permit(john, alice) end @@ -56,13 +56,13 @@ RSpec.describe AccountPolicy do end permissions :redownload? do - context 'admin' do + context 'when admin' do it 'permits' do expect(subject).to permit(admin) end end - context 'not admin' do + context 'when not admin' do it 'denies' do expect(subject).to_not permit(john) end @@ -72,21 +72,21 @@ RSpec.describe AccountPolicy do permissions :suspend?, :silence? do let(:staff) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - context 'staff' do - context 'record is staff' do + context 'when staff' do + context 'when record is staff' do it 'denies' do expect(subject).to_not permit(admin, staff) end end - context 'record is not staff' do + context 'when record is not staff' do it 'permits' do expect(subject).to permit(admin, john) end end end - context 'not staff' do + context 'when not staff' do it 'denies' do expect(subject).to_not permit(john, Account) end @@ -96,21 +96,21 @@ RSpec.describe AccountPolicy do permissions :memorialize? do let(:other_admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - context 'admin' do - context 'record is admin' do + context 'when admin' do + context 'when record is admin' do it 'denies' do expect(subject).to_not permit(admin, other_admin) end end - context 'record is not admin' do + context 'when record is not admin' do it 'permits' do expect(subject).to permit(admin, john) end end end - context 'not admin' do + context 'when not admin' do it 'denies' do expect(subject).to_not permit(john, Account) end @@ -118,13 +118,13 @@ RSpec.describe AccountPolicy do end permissions :review? do - context 'admin' do + context 'when admin' do it 'permits' do expect(subject).to permit(admin) end end - context 'not admin' do + context 'when not admin' do it 'denies' do expect(subject).to_not permit(john) end @@ -132,7 +132,7 @@ RSpec.describe AccountPolicy do end permissions :destroy? do - context 'admin' do + context 'when admin' do context 'with a temporarily suspended account' do before { allow(alice).to receive(:suspended_temporarily?).and_return(true) } @@ -150,7 +150,7 @@ RSpec.describe AccountPolicy do end end - context 'not admin' do + context 'when not admin' do it 'denies' do expect(subject).to_not permit(john, alice) end diff --git a/spec/policies/backup_policy_spec.rb b/spec/policies/backup_policy_spec.rb index 6b31c6f7c7..488d48f52a 100644 --- a/spec/policies/backup_policy_spec.rb +++ b/spec/policies/backup_policy_spec.rb @@ -8,20 +8,20 @@ RSpec.describe BackupPolicy do let(:john) { Fabricate(:account) } permissions :create? do - context 'not user_signed_in?' do + context 'when not user_signed_in?' do it 'denies' do expect(subject).to_not permit(nil, Backup) end end - context 'user_signed_in?' do - context 'no backups' do + context 'when user_signed_in?' do + context 'with no backups' do it 'permits' do expect(subject).to permit(john, Backup) end end - context 'backups are too old' do + context 'when backups are too old' do it 'permits' do travel(-8.days) do Fabricate(:backup, user: john.user) @@ -31,7 +31,7 @@ RSpec.describe BackupPolicy do end end - context 'backups are newer' do + context 'when backups are newer' do it 'denies' do travel(-3.days) do Fabricate(:backup, user: john.user) diff --git a/spec/policies/custom_emoji_policy_spec.rb b/spec/policies/custom_emoji_policy_spec.rb index 6a6ef6694d..cf7e7d924b 100644 --- a/spec/policies/custom_emoji_policy_spec.rb +++ b/spec/policies/custom_emoji_policy_spec.rb @@ -9,13 +9,13 @@ RSpec.describe CustomEmojiPolicy do let(:john) { Fabricate(:account) } permissions :index?, :enable?, :disable? do - context 'staff' do + context 'when staff' do it 'permits' do expect(subject).to permit(admin, CustomEmoji) end end - context 'not staff' do + context 'when not staff' do it 'denies' do expect(subject).to_not permit(john, CustomEmoji) end @@ -23,13 +23,13 @@ RSpec.describe CustomEmojiPolicy do end permissions :create?, :update?, :copy?, :destroy? do - context 'admin' do + context 'when admin' do it 'permits' do expect(subject).to permit(admin, CustomEmoji) end end - context 'not admin' do + context 'when not admin' do it 'denies' do expect(subject).to_not permit(john, CustomEmoji) end diff --git a/spec/policies/domain_block_policy_spec.rb b/spec/policies/domain_block_policy_spec.rb index 01b97e823a..e254e2cf4d 100644 --- a/spec/policies/domain_block_policy_spec.rb +++ b/spec/policies/domain_block_policy_spec.rb @@ -9,13 +9,13 @@ RSpec.describe DomainBlockPolicy do let(:john) { Fabricate(:account) } permissions :index?, :show?, :create?, :destroy? do - context 'admin' do + context 'when admin' do it 'permits' do expect(subject).to permit(admin, DomainBlock) end end - context 'not admin' do + context 'when not admin' do it 'denies' do expect(subject).to_not permit(john, DomainBlock) end diff --git a/spec/policies/email_domain_block_policy_spec.rb b/spec/policies/email_domain_block_policy_spec.rb index e7c455907a..6e57b1372f 100644 --- a/spec/policies/email_domain_block_policy_spec.rb +++ b/spec/policies/email_domain_block_policy_spec.rb @@ -9,13 +9,13 @@ RSpec.describe EmailDomainBlockPolicy do let(:john) { Fabricate(:account) } permissions :index?, :show?, :create?, :destroy? do - context 'admin' do + context 'when admin' do it 'permits' do expect(subject).to permit(admin, EmailDomainBlock) end end - context 'not admin' do + context 'when not admin' do it 'denies' do expect(subject).to_not permit(john, EmailDomainBlock) end diff --git a/spec/policies/instance_policy_spec.rb b/spec/policies/instance_policy_spec.rb index f6f51af068..3e047bbe9e 100644 --- a/spec/policies/instance_policy_spec.rb +++ b/spec/policies/instance_policy_spec.rb @@ -9,13 +9,13 @@ RSpec.describe InstancePolicy do let(:john) { Fabricate(:account) } permissions :index?, :show?, :destroy? do - context 'admin' do + context 'when admin' do it 'permits' do expect(subject).to permit(admin, Instance) end end - context 'not admin' do + context 'when not admin' do it 'denies' do expect(subject).to_not permit(john, Instance) end diff --git a/spec/policies/invite_policy_spec.rb b/spec/policies/invite_policy_spec.rb index 01660322f1..50a312f44f 100644 --- a/spec/policies/invite_policy_spec.rb +++ b/spec/policies/invite_policy_spec.rb @@ -9,7 +9,7 @@ RSpec.describe InvitePolicy do let(:john) { Fabricate(:user).account } permissions :index? do - context 'staff?' do + context 'when staff?' do it 'permits' do expect(subject).to permit(admin, Invite) end @@ -17,7 +17,7 @@ RSpec.describe InvitePolicy do end permissions :create? do - context 'has privilege' do + context 'with privilege' do before do UserRole.everyone.update(permissions: UserRole::FLAGS[:invite_users]) end @@ -27,7 +27,7 @@ RSpec.describe InvitePolicy do end end - context 'does not have privilege' do + context 'when does not have privilege' do before do UserRole.everyone.update(permissions: UserRole::Flags::NONE) end @@ -39,13 +39,13 @@ RSpec.describe InvitePolicy do end permissions :deactivate_all? do - context 'admin?' do + context 'when admin?' do it 'permits' do expect(subject).to permit(admin, Invite) end end - context 'not admin?' do + context 'when not admin?' do it 'denies' do expect(subject).to_not permit(john, Invite) end @@ -53,20 +53,20 @@ RSpec.describe InvitePolicy do end permissions :destroy? do - context 'owner?' do + context 'when owner?' do it 'permits' do expect(subject).to permit(john, Fabricate(:invite, user: john.user)) end end - context 'not owner?' do - context 'admin?' do + context 'when not owner?' do + context 'when admin?' do it 'permits' do expect(subject).to permit(admin, Fabricate(:invite)) end end - context 'not admin?' do + context 'when not admin?' do it 'denies' do expect(subject).to_not permit(john, Fabricate(:invite)) end diff --git a/spec/policies/relay_policy_spec.rb b/spec/policies/relay_policy_spec.rb index 2c50ba1e9f..0d479e0ca7 100644 --- a/spec/policies/relay_policy_spec.rb +++ b/spec/policies/relay_policy_spec.rb @@ -9,13 +9,13 @@ RSpec.describe RelayPolicy do let(:john) { Fabricate(:account) } permissions :update? do - context 'admin?' do + context 'when admin?' do it 'permits' do expect(subject).to permit(admin, Relay) end end - context '!admin?' do + context 'with !admin?' do it 'denies' do expect(subject).to_not permit(john, Relay) end diff --git a/spec/policies/report_note_policy_spec.rb b/spec/policies/report_note_policy_spec.rb index 99f5ffb8e3..ea2a62ada9 100644 --- a/spec/policies/report_note_policy_spec.rb +++ b/spec/policies/report_note_policy_spec.rb @@ -9,13 +9,13 @@ RSpec.describe ReportNotePolicy do let(:john) { Fabricate(:account) } permissions :create? do - context 'staff?' do + context 'when staff?' do it 'permits' do expect(subject).to permit(admin, ReportNote) end end - context '!staff?' do + context 'with !staff?' do it 'denies' do expect(subject).to_not permit(john, ReportNote) end @@ -23,22 +23,22 @@ RSpec.describe ReportNotePolicy do end permissions :destroy? do - context 'admin?' do + context 'when admin?' do it 'permit' do report_note = Fabricate(:report_note, account: john) expect(subject).to permit(admin, report_note) end end - context 'admin?' do - context 'owner?' do + context 'when admin?' do + context 'when owner?' do it 'permit' do report_note = Fabricate(:report_note, account: john) expect(subject).to permit(john, report_note) end end - context '!owner?' do + context 'with !owner?' do it 'denies' do report_note = Fabricate(:report_note) expect(subject).to_not permit(john, report_note) diff --git a/spec/policies/report_policy_spec.rb b/spec/policies/report_policy_spec.rb index 8b005d8ddd..8f2533fa6b 100644 --- a/spec/policies/report_policy_spec.rb +++ b/spec/policies/report_policy_spec.rb @@ -9,13 +9,13 @@ RSpec.describe ReportPolicy do let(:john) { Fabricate(:account) } permissions :update?, :index?, :show? do - context 'staff?' do + context 'when staff?' do it 'permits' do expect(subject).to permit(admin, Report) end end - context '!staff?' do + context 'with !staff?' do it 'denies' do expect(subject).to_not permit(john, Report) end diff --git a/spec/policies/settings_policy_spec.rb b/spec/policies/settings_policy_spec.rb index 3268c16225..576bfa4ab7 100644 --- a/spec/policies/settings_policy_spec.rb +++ b/spec/policies/settings_policy_spec.rb @@ -9,13 +9,13 @@ RSpec.describe SettingsPolicy do let(:john) { Fabricate(:account) } permissions :update?, :show?, :destroy? do - context 'admin?' do + context 'when admin?' do it 'permits' do expect(subject).to permit(admin, Settings) end end - context '!admin?' do + context 'with !admin?' do it 'denies' do expect(subject).to_not permit(john, Settings) end diff --git a/spec/policies/tag_policy_spec.rb b/spec/policies/tag_policy_spec.rb index fb09fdd3be..7791cde152 100644 --- a/spec/policies/tag_policy_spec.rb +++ b/spec/policies/tag_policy_spec.rb @@ -9,13 +9,13 @@ RSpec.describe TagPolicy do let(:john) { Fabricate(:account) } permissions :index?, :show?, :update?, :review? do - context 'staff?' do + context 'when staff?' do it 'permits' do expect(subject).to permit(admin, Tag) end end - context '!staff?' do + context 'with !staff?' do it 'denies' do expect(subject).to_not permit(john, Tag) end diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb index ff0916674e..384119f250 100644 --- a/spec/policies/user_policy_spec.rb +++ b/spec/policies/user_policy_spec.rb @@ -9,21 +9,21 @@ RSpec.describe UserPolicy do let(:john) { Fabricate(:account) } permissions :reset_password?, :change_email? do - context 'staff?' do - context '!record.staff?' do + context 'when staff?' do + context 'with !record.staff?' do it 'permits' do expect(subject).to permit(admin, john.user) end end - context 'record.staff?' do + context 'when record.staff?' do it 'denies' do expect(subject).to_not permit(admin, admin.user) end end end - context '!staff?' do + context 'with !staff?' do it 'denies' do expect(subject).to_not permit(john, User) end @@ -31,21 +31,21 @@ RSpec.describe UserPolicy do end permissions :disable_2fa? do - context 'admin?' do - context '!record.staff?' do + context 'when admin?' do + context 'with !record.staff?' do it 'permits' do expect(subject).to permit(admin, john.user) end end - context 'record.staff?' do + context 'when record.staff?' do it 'denies' do expect(subject).to_not permit(admin, admin.user) end end end - context '!admin?' do + context 'with !admin?' do it 'denies' do expect(subject).to_not permit(john, User) end @@ -53,15 +53,15 @@ RSpec.describe UserPolicy do end permissions :confirm? do - context 'staff?' do - context '!record.confirmed?' do + context 'when staff?' do + context 'with !record.confirmed?' do it 'permits' do john.user.update(confirmed_at: nil) expect(subject).to permit(admin, john.user) end end - context 'record.confirmed?' do + context 'when record.confirmed?' do it 'denies' do john.user.confirm! expect(subject).to_not permit(admin, john.user) @@ -69,7 +69,7 @@ RSpec.describe UserPolicy do end end - context '!staff?' do + context 'with !staff?' do it 'denies' do expect(subject).to_not permit(john, User) end @@ -77,13 +77,13 @@ RSpec.describe UserPolicy do end permissions :enable? do - context 'staff?' do + context 'when staff?' do it 'permits' do expect(subject).to permit(admin, User) end end - context '!staff?' do + context 'with !staff?' do it 'denies' do expect(subject).to_not permit(john, User) end @@ -91,21 +91,21 @@ RSpec.describe UserPolicy do end permissions :disable? do - context 'staff?' do - context '!record.admin?' do + context 'when staff?' do + context 'with !record.admin?' do it 'permits' do expect(subject).to permit(admin, john.user) end end - context 'record.admin?' do + context 'when record.admin?' do it 'denies' do expect(subject).to_not permit(admin, admin.user) end end end - context '!staff?' do + context 'with !staff?' do it 'denies' do expect(subject).to_not permit(john, User) end diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb index 8a485d2b9a..d59060bd5c 100644 --- a/spec/presenters/account_relationships_presenter_spec.rb +++ b/spec/presenters/account_relationships_presenter_spec.rb @@ -19,7 +19,7 @@ RSpec.describe AccountRelationshipsPresenter do let(:account_ids) { [Fabricate(:account).id] } let(:default_map) { { 1 => true } } - context 'options are not set' do + context 'when options are not set' do let(:options) { {} } it 'sets default maps' do @@ -32,7 +32,7 @@ RSpec.describe AccountRelationshipsPresenter do end end - context 'options[:following_map] is set' do + context 'when options[:following_map] is set' do let(:options) { { following_map: { 2 => true } } } it 'sets @following merged with default_map and options[:following_map]' do @@ -40,7 +40,7 @@ RSpec.describe AccountRelationshipsPresenter do end end - context 'options[:followed_by_map] is set' do + context 'when options[:followed_by_map] is set' do let(:options) { { followed_by_map: { 3 => true } } } it 'sets @followed_by merged with default_map and options[:followed_by_map]' do @@ -48,7 +48,7 @@ RSpec.describe AccountRelationshipsPresenter do end end - context 'options[:blocking_map] is set' do + context 'when options[:blocking_map] is set' do let(:options) { { blocking_map: { 4 => true } } } it 'sets @blocking merged with default_map and options[:blocking_map]' do @@ -56,7 +56,7 @@ RSpec.describe AccountRelationshipsPresenter do end end - context 'options[:muting_map] is set' do + context 'when options[:muting_map] is set' do let(:options) { { muting_map: { 5 => true } } } it 'sets @muting merged with default_map and options[:muting_map]' do @@ -64,7 +64,7 @@ RSpec.describe AccountRelationshipsPresenter do end end - context 'options[:requested_map] is set' do + context 'when options[:requested_map] is set' do let(:options) { { requested_map: { 6 => true } } } it 'sets @requested merged with default_map and options[:requested_map]' do @@ -72,7 +72,7 @@ RSpec.describe AccountRelationshipsPresenter do end end - context 'options[:requested_by_map] is set' do + context 'when options[:requested_by_map] is set' do let(:options) { { requested_by_map: { 6 => true } } } it 'sets @requested merged with default_map and options[:requested_by_map]' do @@ -80,7 +80,7 @@ RSpec.describe AccountRelationshipsPresenter do end end - context 'options[:domain_blocking_map] is set' do + context 'when options[:domain_blocking_map] is set' do let(:options) { { domain_blocking_map: { 7 => true } } } it 'sets @domain_blocking merged with default_map and options[:domain_blocking_map]' do diff --git a/spec/presenters/status_relationships_presenter_spec.rb b/spec/presenters/status_relationships_presenter_spec.rb index eaab922fd9..11116cabd2 100644 --- a/spec/presenters/status_relationships_presenter_spec.rb +++ b/spec/presenters/status_relationships_presenter_spec.rb @@ -18,7 +18,7 @@ RSpec.describe StatusRelationshipsPresenter do let(:status_ids) { statuses.map(&:id) + statuses.map(&:reblog_of_id).compact } let(:default_map) { { 1 => true } } - context 'options are not set' do + context 'when options are not set' do let(:options) { {} } it 'sets default maps' do @@ -30,7 +30,7 @@ RSpec.describe StatusRelationshipsPresenter do end end - context 'options[:reblogs_map] is set' do + context 'when options[:reblogs_map] is set' do let(:options) { { reblogs_map: { 2 => true } } } it 'sets @reblogs_map merged with default_map and options[:reblogs_map]' do @@ -38,7 +38,7 @@ RSpec.describe StatusRelationshipsPresenter do end end - context 'options[:favourites_map] is set' do + context 'when options[:favourites_map] is set' do let(:options) { { favourites_map: { 3 => true } } } it 'sets @favourites_map merged with default_map and options[:favourites_map]' do @@ -46,7 +46,7 @@ RSpec.describe StatusRelationshipsPresenter do end end - context 'options[:bookmarks_map] is set' do + context 'when options[:bookmarks_map] is set' do let(:options) { { bookmarks_map: { 4 => true } } } it 'sets @bookmarks_map merged with default_map and options[:bookmarks_map]' do @@ -54,7 +54,7 @@ RSpec.describe StatusRelationshipsPresenter do end end - context 'options[:mutes_map] is set' do + context 'when options[:mutes_map] is set' do let(:options) { { mutes_map: { 5 => true } } } it 'sets @mutes_map merged with default_map and options[:mutes_map]' do @@ -62,7 +62,7 @@ RSpec.describe StatusRelationshipsPresenter do end end - context 'options[:pins_map] is set' do + context 'when options[:pins_map] is set' do let(:options) { { pins_map: { 6 => true } } } it 'sets @pins_map merged with default_map and options[:pins_map]' do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 26fc3d9fdf..22078a6cbc 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -12,7 +12,7 @@ require 'paperclip/matchers' require 'capybara/rspec' require 'chewy/rspec' -Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } +Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f } ActiveRecord::Migration.maintain_test_schema! WebMock.disable_net_connect!(allow: Chewy.settings[:host]) @@ -35,8 +35,28 @@ Devise::Test::ControllerHelpers.module_eval do end end +module SignedRequestHelpers + def get(path, headers: nil, sign_with: nil, **args) + return super path, headers: headers, **args if sign_with.nil? + + headers ||= {} + headers['Date'] = Time.now.utc.httpdate + headers['Host'] = ENV.fetch('LOCAL_DOMAIN') + signed_headers = headers.merge('(request-target)' => "get #{path}").slice('(request-target)', 'Host', 'Date') + + key_id = ActivityPub::TagManager.instance.key_uri_for(sign_with) + keypair = sign_with.keypair + signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") + signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) + + headers['Signature'] = "keyId=\"#{key_id}\",algorithm=\"rsa-sha256\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\"" + + super path, headers: headers, **args + end +end + RSpec.configure do |config| - config.fixture_path = "#{Rails.root}/spec/fixtures" + config.fixture_path = Rails.root.join('spec', 'fixtures') config.use_transactional_fixtures = true config.order = 'random' config.infer_spec_type_from_file_location! @@ -46,10 +66,12 @@ RSpec.configure do |config| config.include Devise::Test::ControllerHelpers, type: :helper config.include Devise::Test::ControllerHelpers, type: :view config.include Devise::Test::IntegrationHelpers, type: :feature + config.include Devise::Test::IntegrationHelpers, type: :request config.include Paperclip::Shoulda::Matchers config.include ActiveSupport::Testing::TimeHelpers config.include Chewy::Rspec::Helpers config.include Redisable + config.include SignedRequestHelpers, type: :request config.before :each, type: :feature do https = ENV['LOCAL_HTTPS'] == 'true' diff --git a/spec/requests/cache_spec.rb b/spec/requests/cache_spec.rb new file mode 100644 index 0000000000..902f21db4b --- /dev/null +++ b/spec/requests/cache_spec.rb @@ -0,0 +1,685 @@ +# frozen_string_literal: true + +require 'rails_helper' + +module TestEndpoints + # Endpoints that do not include authorization-dependent results + # and should be cacheable no matter what. + ALWAYS_CACHED = %w( + /.well-known/host-meta + /.well-known/nodeinfo + /nodeinfo/2.0 + /manifest + /custom.css + /actor + /api/v1/instance/extended_description + /api/v1/instance/rules + /api/v1/instance/peers + /api/v1/instance + /api/v2/instance + ).freeze + + # Endpoints that should be cachable when accessed anonymously but have a Vary + # on Cookie to prevent logged-in users from getting values from logged-out cache. + COOKIE_DEPENDENT_CACHABLE = %w( + / + /explore + /public + /about + /privacy-policy + /directory + /@alice + /@alice/110224538612341312 + ).freeze + + # Endpoints that should be cachable when accessed anonymously but have a Vary + # on Authorization to prevent logged-in users from getting values from logged-out cache. + AUTHORIZATION_DEPENDENT_CACHABLE = %w( + /api/v1/accounts/lookup?acct=alice + /api/v1/statuses/110224538612341312 + /api/v1/statuses/110224538612341312/context + /api/v1/polls/12345 + /api/v1/trends/statuses + /api/v1/directory + ).freeze + + # Private status that should only be returned with to a valid signature from + # a specific user. + # Should never be cached. + REQUIRE_SIGNATURE = %w( + /users/alice/statuses/110224538643211312 + ).freeze + + # Pages only available to logged-in users. + # Should never be cached. + REQUIRE_LOGIN = %w( + /settings/preferences/appearance + /settings/profile + /settings/featured_tags + /settings/export + /relationships + /filters + /statuses_cleanup + /auth/edit + /oauth/authorized_applications + /admin/dashboard + ).freeze + + # API endpoints only available to logged-in users. + # Should never be cached. + REQUIRE_TOKEN = %w( + /api/v1/announcements + /api/v1/timelines/home + /api/v1/notifications + /api/v1/bookmarks + /api/v1/favourites + /api/v1/follow_requests + /api/v1/conversations + /api/v1/statuses/110224538643211312 + /api/v1/statuses/110224538643211312/context + /api/v1/lists + /api/v2/filters + ).freeze + + # Pages that are only shown to logged-out users, and should never get cached + # because of CSRF protection. + REQUIRE_LOGGED_OUT = %w( + /invite/abcdef + /auth/sign_in + /auth/sign_up + /auth/password/new + /auth/confirmation/new + ).freeze + + # Non-exhaustive list of endpoints that feature language-dependent results + # and thus need to have a Vary on Accept-Language + LANGUAGE_DEPENDENT = %w( + / + /explore + /about + /api/v1/trends/statuses + ).freeze + + module AuthorizedFetch + # Endpoints that require a signature with AUTHORIZED_FETCH and LIMITED_FEDERATION_MODE + # and thus should not be cached in those modes. + REQUIRE_SIGNATURE = %w( + /users/alice + ).freeze + end + + module DisabledAnonymousAPI + # Endpoints that require a signature with DISALLOW_UNAUTHENTICATED_API_ACCESS + # and thus should not be cached in this mode. + REQUIRE_TOKEN = %w( + /api/v1/custom_emojis + ).freeze + end +end + +describe 'Caching behavior' do + shared_examples 'cachable response' do + it 'does not set cookies' do + expect(response.cookies).to be_empty + end + + it 'sets public cache control' do + # expect(response.cache_control[:max_age]&.to_i).to be_positive + expect(response.cache_control[:public]).to be_truthy + expect(response.cache_control[:private]).to be_falsy + expect(response.cache_control[:no_store]).to be_falsy + expect(response.cache_control[:no_cache]).to be_falsy + end + end + + shared_examples 'non-cacheable response' do + it 'sets private cache control' do + expect(response.cache_control[:private]).to be_truthy + expect(response.cache_control[:no_store]).to be_truthy + end + end + + shared_examples 'non-cacheable error' do + it 'does not return HTTP success' do + expect(response).to_not have_http_status(200) + end + + it 'does not have cache headers' do + expect(response.cache_control[:public]).to be_falsy + end + end + + shared_examples 'language-dependent' do + it 'has a Vary on Accept-Language' do + expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('accept-language') + end + end + + # Enable CSRF protection like it is in production, as it can cause cookies + # to be set and thus mess with cache. + around do |example| + old = ActionController::Base.allow_forgery_protection + ActionController::Base.allow_forgery_protection = true + + example.run + + ActionController::Base.allow_forgery_protection = old + end + + let(:alice) { Fabricate(:account, username: 'alice') } + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Moderator')) } + + before do + # rubocop:disable Style/NumericLiterals + status = Fabricate(:status, account: alice, id: 110224538612341312) + Fabricate(:status, account: alice, id: 110224538643211312, visibility: :private) + Fabricate(:invite, code: 'abcdef') + Fabricate(:poll, status: status, account: alice, id: 12345) + # rubocop:enable Style/NumericLiterals + + user.account.follow!(alice) + end + + context 'when anonymously accessed' do + TestEndpoints::ALWAYS_CACHED.each do |endpoint| + describe endpoint do + before { get endpoint } + + it_behaves_like 'cachable response' + it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) + end + end + + TestEndpoints::COOKIE_DEPENDENT_CACHABLE.each do |endpoint| + describe endpoint do + before { get endpoint } + + it_behaves_like 'cachable response' + + it 'has a Vary on Cookie' do + expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('cookie') + end + + it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) + end + end + + TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint| + describe endpoint do + before { get endpoint } + + it_behaves_like 'cachable response' + + it 'has a Vary on Authorization' do + expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('authorization') + end + + it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) + end + end + + TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint| + describe endpoint do + before { get endpoint } + + it_behaves_like 'non-cacheable response' + end + end + + (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::REQUIRE_LOGIN + TestEndpoints::REQUIRE_TOKEN).each do |endpoint| + describe endpoint do + before { get endpoint } + + it_behaves_like 'non-cacheable error' + end + end + + describe '/api/v1/instance/domain_blocks' do + around do |example| + old_setting = Setting.show_domain_blocks + Setting.show_domain_blocks = show_domain_blocks + + example.run + + Setting.show_domain_blocks = old_setting + end + + before { get '/api/v1/instance/domain_blocks' } + + context 'when set to be publicly-available' do + let(:show_domain_blocks) { 'all' } + + it_behaves_like 'cachable response' + end + + context 'when allowed for local users only' do + let(:show_domain_blocks) { 'users' } + + it_behaves_like 'non-cacheable error' + end + + context 'when disabled' do + let(:show_domain_blocks) { 'disabled' } + + it_behaves_like 'non-cacheable error' + end + end + end + + context 'when logged in' do + before do + sign_in user, scope: :user + + # Unfortunately, devise's `sign_in` helper causes the `session` to be + # loaded in the next request regardless of whether it's actually accessed + # by the client code. + # + # So, we make an extra query to clear issue a session cookie instead. + # + # A less resource-intensive way to deal with that would be to generate the + # session cookie manually, but this seems pretty involved. + get '/' + end + + TestEndpoints::ALWAYS_CACHED.each do |endpoint| + describe endpoint do + before { get endpoint } + + it_behaves_like 'cachable response' + it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) + end + end + + TestEndpoints::COOKIE_DEPENDENT_CACHABLE.each do |endpoint| + describe endpoint do + before { get endpoint } + + it_behaves_like 'non-cacheable response' + + it 'has a Vary on Cookie' do + expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('cookie') + end + end + end + + TestEndpoints::REQUIRE_LOGIN.each do |endpoint| + describe endpoint do + before { get endpoint } + + it_behaves_like 'non-cacheable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + end + + TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint| + describe endpoint do + before { get endpoint } + + it_behaves_like 'non-cacheable error' + end + end + end + + context 'with an auth token' do + let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + + TestEndpoints::ALWAYS_CACHED.each do |endpoint| + describe endpoint do + before do + get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } + end + + it_behaves_like 'cachable response' + it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) + end + end + + TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint| + describe endpoint do + before do + get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } + end + + it_behaves_like 'non-cacheable response' + + it 'has a Vary on Authorization' do + expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('authorization') + end + end + end + + (TestEndpoints::REQUIRE_LOGGED_OUT + TestEndpoints::REQUIRE_TOKEN).each do |endpoint| + describe endpoint do + before do + get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } + end + + it_behaves_like 'non-cacheable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + end + + describe '/api/v1/instance/domain_blocks' do + around do |example| + old_setting = Setting.show_domain_blocks + Setting.show_domain_blocks = show_domain_blocks + + example.run + + Setting.show_domain_blocks = old_setting + end + + before do + get '/api/v1/instance/domain_blocks', headers: { 'Authorization' => "Bearer #{token.token}" } + end + + context 'when set to be publicly-available' do + let(:show_domain_blocks) { 'all' } + + it_behaves_like 'cachable response' + end + + context 'when allowed for local users only' do + let(:show_domain_blocks) { 'users' } + + it_behaves_like 'non-cacheable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + + context 'when disabled' do + let(:show_domain_blocks) { 'disabled' } + + it_behaves_like 'non-cacheable error' + end + end + end + + context 'with a Signature header' do + let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) } + let(:dummy_signature) { 'dummy-signature' } + + before do + remote_actor.follow!(alice) + end + + describe '/actor' do + before do + get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } + end + + it_behaves_like 'cachable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + + TestEndpoints::REQUIRE_SIGNATURE.each do |endpoint| + describe endpoint do + before do + get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } + end + + it_behaves_like 'non-cacheable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + end + end + + context 'when enabling AUTHORIZED_FETCH mode' do + around do |example| + ClimateControl.modify AUTHORIZED_FETCH: 'true' do + example.run + end + end + + context 'when not providing a Signature' do + describe '/actor' do + before do + get '/actor', headers: { 'Accept' => 'application/activity+json' } + end + + it_behaves_like 'cachable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + + (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| + describe endpoint do + before do + get endpoint, headers: { 'Accept' => 'application/activity+json' } + end + + it_behaves_like 'non-cacheable error' + end + end + end + + context 'when providing a Signature' do + let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) } + let(:dummy_signature) { 'dummy-signature' } + + before do + remote_actor.follow!(alice) + end + + describe '/actor' do + before do + get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } + end + + it_behaves_like 'cachable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + + (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| + describe endpoint do + before do + get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } + end + + it_behaves_like 'non-cacheable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + end + end + end + + context 'when enabling LIMITED_FEDERATION_MODE mode' do + around do |example| + ClimateControl.modify LIMITED_FEDERATION_MODE: 'true' do + old_whitelist_mode = Rails.configuration.x.whitelist_mode + Rails.configuration.x.whitelist_mode = true + + example.run + + Rails.configuration.x.whitelist_mode = old_whitelist_mode + end + end + + context 'when not providing a Signature' do + describe '/actor' do + before do + get '/actor', headers: { 'Accept' => 'application/activity+json' } + end + + it_behaves_like 'cachable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + + (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| + describe endpoint do + before do + get endpoint, headers: { 'Accept' => 'application/activity+json' } + end + + it_behaves_like 'non-cacheable error' + end + end + end + + context 'when providing a Signature from an allowed domain' do + let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) } + let(:dummy_signature) { 'dummy-signature' } + + before do + DomainAllow.create!(domain: remote_actor.domain) + remote_actor.follow!(alice) + end + + describe '/actor' do + before do + get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } + end + + it_behaves_like 'cachable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + + (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| + describe endpoint do + before do + get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } + end + + it_behaves_like 'non-cacheable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + end + end + + context 'when providing a Signature from a non-allowed domain' do + let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) } + let(:dummy_signature) { 'dummy-signature' } + + describe '/actor' do + before do + get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } + end + + it_behaves_like 'cachable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + + (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| + describe endpoint do + before do + get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } + end + + it_behaves_like 'non-cacheable error' + end + end + end + end + + context 'when enabling DISALLOW_UNAUTHENTICATED_API_ACCESS' do + around do |example| + ClimateControl.modify DISALLOW_UNAUTHENTICATED_API_ACCESS: 'true' do + example.run + end + end + + context 'when anonymously accessed' do + TestEndpoints::ALWAYS_CACHED.each do |endpoint| + describe endpoint do + before { get endpoint } + + it_behaves_like 'cachable response' + it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) + end + end + + TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint| + describe endpoint do + before { get endpoint } + + it_behaves_like 'non-cacheable response' + end + end + + (TestEndpoints::REQUIRE_TOKEN + TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE + TestEndpoints::DisabledAnonymousAPI::REQUIRE_TOKEN).each do |endpoint| + describe endpoint do + before { get endpoint } + + it_behaves_like 'non-cacheable error' + end + end + end + + context 'with an auth token' do + let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + + TestEndpoints::ALWAYS_CACHED.each do |endpoint| + describe endpoint do + before do + get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } + end + + it_behaves_like 'cachable response' + it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) + end + end + + TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint| + describe endpoint do + before do + get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } + end + + it_behaves_like 'non-cacheable response' + + it 'has a Vary on Authorization' do + expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('authorization') + end + end + end + + (TestEndpoints::REQUIRE_LOGGED_OUT + TestEndpoints::REQUIRE_TOKEN + TestEndpoints::DisabledAnonymousAPI::REQUIRE_TOKEN).each do |endpoint| + describe endpoint do + before do + get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } + end + + it_behaves_like 'non-cacheable response' + + it 'returns HTTP success' do + expect(response).to have_http_status(200) + end + end + end + end + end +end diff --git a/spec/requests/link_headers_spec.rb b/spec/requests/link_headers_spec.rb index c32e0f79a9..b822adbfb8 100644 --- a/spec/requests/link_headers_spec.rb +++ b/spec/requests/link_headers_spec.rb @@ -13,7 +13,7 @@ describe 'Link headers' do it 'contains webfinger url in link header' do link_header = link_header_with_type('application/jrd+json') - expect(link_header.href).to match 'http://www.example.com/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io' + expect(link_header.href).to eq 'http://www.example.com/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io' expect(link_header.attr_pairs.first).to eq %w(rel lrdd) end @@ -26,7 +26,7 @@ describe 'Link headers' do def link_header_with_type(type) LinkHeader.parse(response.headers['Link'].to_s).links.find do |link| - link.attr_pairs.any? { |pair| pair == ['type', type] } + link.attr_pairs.any?(['type', type]) end end end diff --git a/spec/services/account_search_service_spec.rb b/spec/services/account_search_service_spec.rb index bb819bb6c0..98264e6e13 100644 --- a/spec/services/account_search_service_spec.rb +++ b/spec/services/account_search_service_spec.rb @@ -20,7 +20,7 @@ describe AccountSearchService, type: :service do end end - context 'searching for a simple term that is not an exact match' do + context 'when searching for a simple term that is not an exact match' do it 'does not return a nil entry in the array for the exact match' do account = Fabricate(:account, username: 'matchingusername') results = subject.call('match', nil, limit: 5) diff --git a/spec/services/account_statuses_cleanup_service_spec.rb b/spec/services/account_statuses_cleanup_service_spec.rb index e83063f734..f7a88a9172 100644 --- a/spec/services/account_statuses_cleanup_service_spec.rb +++ b/spec/services/account_statuses_cleanup_service_spec.rb @@ -20,13 +20,13 @@ describe AccountStatusesCleanupService, type: :service do let!(:another_old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } let!(:recent_status) { Fabricate(:status, created_at: 1.day.ago, account: account) } - context 'given a budget of 1' do + context 'when given a budget of 1' do it 'reports 1 deleted toot' do expect(subject.call(account_policy, 1)).to eq 1 end end - context 'given a normal budget of 10' do + context 'when given a normal budget of 10' do it 'reports 3 deleted statuses' do expect(subject.call(account_policy, 10)).to eq 3 end diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 1c39db21fc..826b67d884 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -226,12 +226,12 @@ RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do end end - context 'statuses referencing other statuses' do + context 'with statuses referencing other statuses' do before do stub_const 'ActivityPub::FetchRemoteStatusService::DISCOVERIES_PER_REQUEST', 5 end - context 'using inReplyTo' do + context 'when using inReplyTo' do let(:object) do { '@context': 'https://www.w3.org/ns/activitystreams', @@ -267,7 +267,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do end end - context 'using replies' do + context 'when using replies' do let(:object) do { '@context': 'https://www.w3.org/ns/activitystreams', diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb index 491b8ed5af..ffbc51b641 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' RSpec.describe ActivityPub::ProcessAccountService, type: :service do subject { described_class.new } - context 'property values' do + context 'with property values' do let(:payload) do { id: 'https://foo.test', @@ -82,7 +82,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do account.suspend!(origin: suspension_origin) end - context 'locally' do + context 'when locally' do let(:suspension_origin) { :local } it 'does not unsuspend it' do @@ -94,7 +94,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do end end - context 'remotely' do + context 'when remotely' do let(:suspension_origin) { :remote } it 'unsuspends it' do @@ -112,7 +112,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do end end - context 'discovering many subdomains in a short timeframe' do + context 'when discovering many subdomains in a short timeframe' do before do stub_const 'ActivityPub::ProcessAccountService::SUBDOMAINS_RATELIMIT', 5 end @@ -138,7 +138,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do end end - context 'accounts referencing other accounts' do + context 'when Accounts referencing other accounts' do before do stub_const 'ActivityPub::ProcessAccountService::DISCOVERIES_PER_REQUEST', 5 end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index e9f23b9cf2..9d90e5eb80 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -269,7 +269,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do end end - context 'originally without tags' do + context 'when originally without tags' do before do subject.call(status, json) end @@ -279,7 +279,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do end end - context 'originally with tags' do + context 'when originally with tags' do let(:tags) { [Fabricate(:tag, name: 'test'), Fabricate(:tag, name: 'foo')] } let(:payload) do @@ -305,7 +305,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do end end - context 'originally without mentions' do + context 'when originally without mentions' do before do subject.call(status, json) end @@ -315,7 +315,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do end end - context 'originally with mentions' do + context 'when originally with mentions' do let(:mentions) { [alice, bob] } before do @@ -327,7 +327,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do end end - context 'originally without media attachments' do + context 'when originally without media attachments' do before do stub_request(:get, 'https://example.com/foo.png').to_return(body: attachment_fixture('emojo.png')) subject.call(status, json) @@ -362,7 +362,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do end end - context 'originally with media attachments' do + context 'when originally with media attachments' do let(:media_attachments) { [Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png'), Fabricate(:media_attachment, remote_url: 'https://example.com/unused.png')] } let(:payload) do @@ -404,7 +404,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do end end - context 'originally with a poll' do + context 'when originally with a poll' do before do poll = Fabricate(:poll, status: status) status.update(preloadable_poll: poll) @@ -420,7 +420,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do end end - context 'originally without a poll' do + context 'when originally without a poll' do let(:payload) do { '@context': 'https://www.w3.org/ns/activitystreams', diff --git a/spec/services/backup_service_spec.rb b/spec/services/backup_service_spec.rb new file mode 100644 index 0000000000..b961b7f675 --- /dev/null +++ b/spec/services/backup_service_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe BackupService, type: :service do + subject(:service_call) { described_class.new.call(backup) } + + let!(:user) { Fabricate(:user) } + let!(:attachment) { Fabricate(:media_attachment, account: user.account) } + let!(:status) { Fabricate(:status, account: user.account, text: 'Hello', visibility: :public, media_attachments: [attachment]) } + let!(:private_status) { Fabricate(:status, account: user.account, text: 'secret', visibility: :private) } + let!(:favourite) { Fabricate(:favourite, account: user.account) } + let!(:bookmark) { Fabricate(:bookmark, account: user.account) } + let!(:backup) { Fabricate(:backup, user: user) } + + def read_zip_file(backup, filename) + file = Paperclip.io_adapters.for(backup.dump) + Zip::File.open(file) do |zipfile| + entry = zipfile.glob(filename).first + return entry.get_input_stream.read + end + end + + it 'marks the backup as processed' do + expect { service_call }.to change(backup, :processed).from(false).to(true) + end + + it 'exports outbox.json as expected' do + service_call + + json = Oj.load(read_zip_file(backup, 'outbox.json')) + expect(json['@context']).to_not be_nil + expect(json['type']).to eq 'OrderedCollection' + expect(json['totalItems']).to eq 2 + expect(json['orderedItems'][0]['@context']).to be_nil + expect(json['orderedItems'][0]).to include({ + 'type' => 'Create', + 'object' => include({ + 'id' => ActivityPub::TagManager.instance.uri_for(status), + 'content' => '<p>Hello</p>', + }), + }) + expect(json['orderedItems'][1]).to include({ + 'type' => 'Create', + 'object' => include({ + 'id' => ActivityPub::TagManager.instance.uri_for(private_status), + 'content' => '<p>secret</p>', + }), + }) + end + + it 'exports likes.json as expected' do + service_call + + json = Oj.load(read_zip_file(backup, 'likes.json')) + expect(json['type']).to eq 'OrderedCollection' + expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(favourite.status)] + end + + it 'exports bookmarks.json as expected' do + service_call + + json = Oj.load(read_zip_file(backup, 'bookmarks.json')) + expect(json['type']).to eq 'OrderedCollection' + expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(bookmark.status)] + end +end diff --git a/spec/services/bulk_import_row_service_spec.rb b/spec/services/bulk_import_row_service_spec.rb new file mode 100644 index 0000000000..5bbe6b0042 --- /dev/null +++ b/spec/services/bulk_import_row_service_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe BulkImportRowService do + subject { described_class.new } + + let(:account) { Fabricate(:account) } + let(:import) { Fabricate(:bulk_import, account: account, type: import_type) } + let(:import_row) { Fabricate(:bulk_import_row, bulk_import: import, data: data) } + + describe '#call' do + context 'when importing a follow' do + let(:import_type) { 'following' } + let(:target_account) { Fabricate(:account) } + let(:service_double) { instance_double(FollowService, call: nil) } + let(:data) do + { 'acct' => target_account.acct } + end + + before do + allow(FollowService).to receive(:new).and_return(service_double) + end + + it 'calls FollowService with the expected arguments and returns true' do + expect(subject.call(import_row)).to be true + + expect(service_double).to have_received(:call).with(account, target_account, { reblogs: nil, notify: nil, languages: nil }) + end + end + + context 'when importing a block' do + let(:import_type) { 'blocking' } + let(:target_account) { Fabricate(:account) } + let(:service_double) { instance_double(BlockService, call: nil) } + let(:data) do + { 'acct' => target_account.acct } + end + + before do + allow(BlockService).to receive(:new).and_return(service_double) + end + + it 'calls BlockService with the expected arguments and returns true' do + expect(subject.call(import_row)).to be true + + expect(service_double).to have_received(:call).with(account, target_account) + end + end + + context 'when importing a mute' do + let(:import_type) { 'muting' } + let(:target_account) { Fabricate(:account) } + let(:service_double) { instance_double(MuteService, call: nil) } + let(:data) do + { 'acct' => target_account.acct } + end + + before do + allow(MuteService).to receive(:new).and_return(service_double) + end + + it 'calls MuteService with the expected arguments and returns true' do + expect(subject.call(import_row)).to be true + + expect(service_double).to have_received(:call).with(account, target_account, { notifications: nil }) + end + end + + context 'when importing a bookmark' do + let(:import_type) { 'bookmarks' } + let(:data) do + { 'uri' => ActivityPub::TagManager.instance.uri_for(target_status) } + end + + context 'when the status is public' do + let(:target_status) { Fabricate(:status) } + + it 'bookmarks the status and returns true' do + expect(subject.call(import_row)).to be true + expect(account.bookmarked?(target_status)).to be true + end + end + + context 'when the status is not accessible to the user' do + let(:target_status) { Fabricate(:status, visibility: :direct) } + + it 'does not bookmark the status and returns false' do + expect(subject.call(import_row)).to be false + expect(account.bookmarked?(target_status)).to be false + end + end + end + end +end diff --git a/spec/services/bulk_import_service_spec.rb b/spec/services/bulk_import_service_spec.rb new file mode 100644 index 0000000000..09dfb0a0b6 --- /dev/null +++ b/spec/services/bulk_import_service_spec.rb @@ -0,0 +1,417 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe BulkImportService do + subject { described_class.new } + + let(:account) { Fabricate(:account) } + let(:import) { Fabricate(:bulk_import, account: account, type: import_type, overwrite: overwrite, state: :in_progress, imported_items: 0, processed_items: 0) } + + before do + import.update(total_items: import.rows.count) + end + + describe '#call' do + around do |example| + Sidekiq::Testing.fake! do + example.run + Sidekiq::Worker.clear_all + end + end + + context 'when importing follows' do + let(:import_type) { 'following' } + let(:overwrite) { false } + + let!(:rows) do + [ + { 'acct' => 'user@foo.bar' }, + { 'acct' => 'unknown@unknown.bar' }, + ].map { |data| import.rows.create!(data: data) } + end + + before do + account.follow!(Fabricate(:account)) + end + + it 'does not immediately change who the account follows' do + expect { subject.call(import) }.to_not(change { account.reload.active_relationships.to_a }) + end + + it 'enqueues workers for the expected rows' do + subject.call(import) + expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) + end + + it 'requests to follow all the listed users once the workers have run' do + subject.call(import) + + resolve_account_service_double = double + allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) + allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } + allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } + + Import::RowWorker.drain + + expect(FollowRequest.includes(:target_account).where(account: account).map(&:target_account).map(&:acct)).to contain_exactly('user@foo.bar', 'unknown@unknown.bar') + end + end + + context 'when importing follows with overwrite' do + let(:import_type) { 'following' } + let(:overwrite) { true } + + let!(:followed) { Fabricate(:account, username: 'followed', domain: 'foo.bar', protocol: :activitypub) } + let!(:to_be_unfollowed) { Fabricate(:account, username: 'to_be_unfollowed', domain: 'foo.bar', protocol: :activitypub) } + + let!(:rows) do + [ + { 'acct' => 'followed@foo.bar', 'show_reblogs' => false, 'notify' => true, 'languages' => ['en'] }, + { 'acct' => 'user@foo.bar' }, + { 'acct' => 'unknown@unknown.bar' }, + ].map { |data| import.rows.create!(data: data) } + end + + before do + account.follow!(followed, reblogs: true, notify: false) + account.follow!(to_be_unfollowed) + end + + it 'unfollows user not present on list' do + subject.call(import) + expect(account.following?(to_be_unfollowed)).to be false + end + + it 'updates the existing follow relationship as expected' do + expect { subject.call(import) }.to change { Follow.where(account: account, target_account: followed).pick(:show_reblogs, :notify, :languages) }.from([true, false, nil]).to([false, true, ['en']]) + end + + it 'enqueues workers for the expected rows' do + subject.call(import) + expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows[1..].map(&:id)) + end + + it 'requests to follow all the expected users once the workers have run' do + subject.call(import) + + resolve_account_service_double = double + allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) + allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } + allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } + + Import::RowWorker.drain + + expect(FollowRequest.includes(:target_account).where(account: account).map(&:target_account).map(&:acct)).to contain_exactly('user@foo.bar', 'unknown@unknown.bar') + end + end + + context 'when importing blocks' do + let(:import_type) { 'blocking' } + let(:overwrite) { false } + + let!(:rows) do + [ + { 'acct' => 'user@foo.bar' }, + { 'acct' => 'unknown@unknown.bar' }, + ].map { |data| import.rows.create!(data: data) } + end + + before do + account.block!(Fabricate(:account, username: 'already_blocked', domain: 'remote.org')) + end + + it 'does not immediately change who the account blocks' do + expect { subject.call(import) }.to_not(change { account.reload.blocking.to_a }) + end + + it 'enqueues workers for the expected rows' do + subject.call(import) + expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) + end + + it 'blocks all the listed users once the workers have run' do + subject.call(import) + + resolve_account_service_double = double + allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) + allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } + allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } + + Import::RowWorker.drain + + expect(account.blocking.map(&:acct)).to contain_exactly('already_blocked@remote.org', 'user@foo.bar', 'unknown@unknown.bar') + end + end + + context 'when importing blocks with overwrite' do + let(:import_type) { 'blocking' } + let(:overwrite) { true } + + let!(:blocked) { Fabricate(:account, username: 'blocked', domain: 'foo.bar', protocol: :activitypub) } + let!(:to_be_unblocked) { Fabricate(:account, username: 'to_be_unblocked', domain: 'foo.bar', protocol: :activitypub) } + + let!(:rows) do + [ + { 'acct' => 'blocked@foo.bar' }, + { 'acct' => 'user@foo.bar' }, + { 'acct' => 'unknown@unknown.bar' }, + ].map { |data| import.rows.create!(data: data) } + end + + before do + account.block!(blocked) + account.block!(to_be_unblocked) + end + + it 'unblocks user not present on list' do + subject.call(import) + expect(account.blocking?(to_be_unblocked)).to be false + end + + it 'enqueues workers for the expected rows' do + subject.call(import) + expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows[1..].map(&:id)) + end + + it 'requests to follow all the expected users once the workers have run' do + subject.call(import) + + resolve_account_service_double = double + allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) + allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } + allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } + + Import::RowWorker.drain + + expect(account.blocking.map(&:acct)).to contain_exactly('blocked@foo.bar', 'user@foo.bar', 'unknown@unknown.bar') + end + end + + context 'when importing mutes' do + let(:import_type) { 'muting' } + let(:overwrite) { false } + + let!(:rows) do + [ + { 'acct' => 'user@foo.bar' }, + { 'acct' => 'unknown@unknown.bar' }, + ].map { |data| import.rows.create!(data: data) } + end + + before do + account.mute!(Fabricate(:account, username: 'already_muted', domain: 'remote.org')) + end + + it 'does not immediately change who the account blocks' do + expect { subject.call(import) }.to_not(change { account.reload.muting.to_a }) + end + + it 'enqueues workers for the expected rows' do + subject.call(import) + expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) + end + + it 'mutes all the listed users once the workers have run' do + subject.call(import) + + resolve_account_service_double = double + allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) + allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } + allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } + + Import::RowWorker.drain + + expect(account.muting.map(&:acct)).to contain_exactly('already_muted@remote.org', 'user@foo.bar', 'unknown@unknown.bar') + end + end + + context 'when importing mutes with overwrite' do + let(:import_type) { 'muting' } + let(:overwrite) { true } + + let!(:muted) { Fabricate(:account, username: 'muted', domain: 'foo.bar', protocol: :activitypub) } + let!(:to_be_unmuted) { Fabricate(:account, username: 'to_be_unmuted', domain: 'foo.bar', protocol: :activitypub) } + + let!(:rows) do + [ + { 'acct' => 'muted@foo.bar', 'hide_notifications' => true }, + { 'acct' => 'user@foo.bar' }, + { 'acct' => 'unknown@unknown.bar' }, + ].map { |data| import.rows.create!(data: data) } + end + + before do + account.mute!(muted, notifications: false) + account.mute!(to_be_unmuted) + end + + it 'updates the existing mute as expected' do + expect { subject.call(import) }.to change { Mute.where(account: account, target_account: muted).pick(:hide_notifications) }.from(false).to(true) + end + + it 'unblocks user not present on list' do + subject.call(import) + expect(account.muting?(to_be_unmuted)).to be false + end + + it 'enqueues workers for the expected rows' do + subject.call(import) + expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows[1..].map(&:id)) + end + + it 'requests to follow all the expected users once the workers have run' do + subject.call(import) + + resolve_account_service_double = double + allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) + allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } + allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } + + Import::RowWorker.drain + + expect(account.muting.map(&:acct)).to contain_exactly('muted@foo.bar', 'user@foo.bar', 'unknown@unknown.bar') + end + end + + context 'when importing domain blocks' do + let(:import_type) { 'domain_blocking' } + let(:overwrite) { false } + + let!(:rows) do + [ + { 'domain' => 'blocked.com' }, + { 'domain' => 'to_block.com' }, + ].map { |data| import.rows.create!(data: data) } + end + + before do + account.block_domain!('alreadyblocked.com') + account.block_domain!('blocked.com') + end + + it 'blocks all the new domains' do + subject.call(import) + expect(account.domain_blocks.pluck(:domain)).to contain_exactly('alreadyblocked.com', 'blocked.com', 'to_block.com') + end + + it 'marks the import as finished' do + subject.call(import) + expect(import.reload.finished?).to be true + end + end + + context 'when importing domain blocks with overwrite' do + let(:import_type) { 'domain_blocking' } + let(:overwrite) { true } + + let!(:rows) do + [ + { 'domain' => 'blocked.com' }, + { 'domain' => 'to_block.com' }, + ].map { |data| import.rows.create!(data: data) } + end + + before do + account.block_domain!('alreadyblocked.com') + account.block_domain!('blocked.com') + end + + it 'blocks all the new domains' do + subject.call(import) + expect(account.domain_blocks.pluck(:domain)).to contain_exactly('blocked.com', 'to_block.com') + end + + it 'marks the import as finished' do + subject.call(import) + expect(import.reload.finished?).to be true + end + end + + context 'when importing bookmarks' do + let(:import_type) { 'bookmarks' } + let(:overwrite) { false } + + let!(:already_bookmarked) { Fabricate(:status, uri: 'https://already.bookmarked/1') } + let!(:status) { Fabricate(:status, uri: 'https://foo.bar/posts/1') } + let!(:inaccessible_status) { Fabricate(:status, uri: 'https://foo.bar/posts/inaccessible', visibility: :direct) } + let!(:bookmarked) { Fabricate(:status, uri: 'https://foo.bar/posts/already-bookmarked') } + + let!(:rows) do + [ + { 'uri' => status.uri }, + { 'uri' => inaccessible_status.uri }, + { 'uri' => bookmarked.uri }, + { 'uri' => 'https://domain.unknown/foo' }, + { 'uri' => 'https://domain.unknown/private' }, + ].map { |data| import.rows.create!(data: data) } + end + + before do + account.bookmarks.create!(status: already_bookmarked) + account.bookmarks.create!(status: bookmarked) + end + + it 'enqueues workers for the expected rows' do + subject.call(import) + expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) + end + + it 'updates the bookmarks as expected once the workers have run' do + subject.call(import) + + service_double = double + allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_double) + allow(service_double).to receive(:call).with('https://domain.unknown/foo') { Fabricate(:status, uri: 'https://domain.unknown/foo') } + allow(service_double).to receive(:call).with('https://domain.unknown/private') { Fabricate(:status, uri: 'https://domain.unknown/private', visibility: :direct) } + + Import::RowWorker.drain + + expect(account.bookmarks.map(&:status).map(&:uri)).to contain_exactly(already_bookmarked.uri, status.uri, bookmarked.uri, 'https://domain.unknown/foo') + end + end + + context 'when importing bookmarks with overwrite' do + let(:import_type) { 'bookmarks' } + let(:overwrite) { true } + + let!(:already_bookmarked) { Fabricate(:status, uri: 'https://already.bookmarked/1') } + let!(:status) { Fabricate(:status, uri: 'https://foo.bar/posts/1') } + let!(:inaccessible_status) { Fabricate(:status, uri: 'https://foo.bar/posts/inaccessible', visibility: :direct) } + let!(:bookmarked) { Fabricate(:status, uri: 'https://foo.bar/posts/already-bookmarked') } + + let!(:rows) do + [ + { 'uri' => status.uri }, + { 'uri' => inaccessible_status.uri }, + { 'uri' => bookmarked.uri }, + { 'uri' => 'https://domain.unknown/foo' }, + { 'uri' => 'https://domain.unknown/private' }, + ].map { |data| import.rows.create!(data: data) } + end + + before do + account.bookmarks.create!(status: already_bookmarked) + account.bookmarks.create!(status: bookmarked) + end + + it 'enqueues workers for the expected rows' do + subject.call(import) + expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) + end + + it 'updates the bookmarks as expected once the workers have run' do + subject.call(import) + + service_double = double + allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_double) + allow(service_double).to receive(:call).with('https://domain.unknown/foo') { Fabricate(:status, uri: 'https://domain.unknown/foo') } + allow(service_double).to receive(:call).with('https://domain.unknown/private') { Fabricate(:status, uri: 'https://domain.unknown/private', visibility: :direct) } + + Import::RowWorker.drain + + expect(account.bookmarks.map(&:status).map(&:uri)).to contain_exactly(status.uri, bookmarked.uri, 'https://domain.unknown/foo') + end + end + end +end diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb index d79ab7a433..7016ecd3f4 100644 --- a/spec/services/fetch_link_card_service_spec.rb +++ b/spec/services/fetch_link_card_service_spec.rb @@ -18,7 +18,7 @@ RSpec.describe FetchLinkCardService, type: :service do subject.call(status) end - context 'in a local status' do + context 'with a local status' do context do let(:status) { Fabricate(:status, text: 'Check out http://example.中国') } @@ -89,7 +89,7 @@ RSpec.describe FetchLinkCardService, type: :service do end end - context 'in a remote status' do + context 'with a remote status' do let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu <a>foo</a> #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener noreferrer" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen? Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener noreferrer" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener noreferrer" title="http://sn.jonkman.ca/group/416/id">security</a> ') } it 'parses out URLs' do diff --git a/spec/services/fetch_oembed_service_spec.rb b/spec/services/fetch_oembed_service_spec.rb index 8a0b492223..777cbae3fb 100644 --- a/spec/services/fetch_oembed_service_spec.rb +++ b/spec/services/fetch_oembed_service_spec.rb @@ -39,7 +39,7 @@ describe FetchOEmbedService, type: :service do end end - context 'Both of JSON and XML provider are discoverable' do + context 'when both of JSON and XML provider are discoverable' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, @@ -66,7 +66,7 @@ describe FetchOEmbedService, type: :service do end end - context 'JSON provider is discoverable while XML provider is not' do + context 'when JSON provider is discoverable while XML provider is not' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, @@ -87,7 +87,7 @@ describe FetchOEmbedService, type: :service do end end - context 'XML provider is discoverable while JSON provider is not' do + context 'when XML provider is discoverable while JSON provider is not' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, @@ -108,7 +108,7 @@ describe FetchOEmbedService, type: :service do end end - context 'Invalid XML provider is discoverable while JSON provider is not' do + context 'with Invalid XML provider is discoverable while JSON provider is not' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, @@ -122,7 +122,7 @@ describe FetchOEmbedService, type: :service do end end - context 'Neither of JSON and XML provider is discoverable' do + context 'with neither of JSON and XML provider is discoverable' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, @@ -136,7 +136,7 @@ describe FetchOEmbedService, type: :service do end end - context 'Empty JSON provider is discoverable' do + context 'when empty JSON provider is discoverable' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( status: 200, diff --git a/spec/services/fetch_remote_status_service_spec.rb b/spec/services/fetch_remote_status_service_spec.rb index 694a75dc29..798740c9b3 100644 --- a/spec/services/fetch_remote_status_service_spec.rb +++ b/spec/services/fetch_remote_status_service_spec.rb @@ -16,7 +16,7 @@ RSpec.describe FetchRemoteStatusService, type: :service do } end - context 'protocol is :activitypub' do + context 'when protocol is :activitypub' do subject { described_class.new.call(note[:id], prefetched_body: prefetched_body) } let(:prefetched_body) { Oj.dump(note) } diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index 67a8b2c54e..c9521e3c87 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe FollowService, type: :service do let(:sender) { Fabricate(:account, username: 'alice') } - context 'local account' do + context 'when local account' do describe 'locked account' do let(:bob) { Fabricate(:account, locked: true, username: 'bob') } @@ -138,7 +138,7 @@ RSpec.describe FollowService, type: :service do end end - context 'remote ActivityPub account' do + context 'when remote ActivityPub account' do let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } before do diff --git a/spec/services/import_service_spec.rb b/spec/services/import_service_spec.rb index f081f2d9dc..7f8e5855fa 100644 --- a/spec/services/import_service_spec.rb +++ b/spec/services/import_service_spec.rb @@ -13,7 +13,7 @@ RSpec.describe ImportService, type: :service do stub_request(:post, 'https://example.com/inbox').to_return(status: 200) end - context 'import old-style list of muted users' do + context 'when importing old-style list of muted users' do subject { ImportService.new } let(:csv) { attachment_fixture('mute-imports.txt') } @@ -51,7 +51,7 @@ RSpec.describe ImportService, type: :service do end end - context 'import new-style list of muted users' do + context 'when importing new-style list of muted users' do subject { ImportService.new } let(:csv) { attachment_fixture('new-mute-imports.txt') } @@ -92,7 +92,7 @@ RSpec.describe ImportService, type: :service do end end - context 'import old-style list of followed users' do + context 'when importing old-style list of followed users' do subject { ImportService.new } let(:csv) { attachment_fixture('mute-imports.txt') } @@ -134,7 +134,7 @@ RSpec.describe ImportService, type: :service do end end - context 'import new-style list of followed users' do + context 'when importing new-style list of followed users' do subject { ImportService.new } let(:csv) { attachment_fixture('new-following-imports.txt') } @@ -181,7 +181,7 @@ RSpec.describe ImportService, type: :service do # Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users # # https://github.com/mastodon/mastodon/issues/20571 - context 'utf-8 encoded domains' do + context 'with a utf-8 encoded domains' do subject { ImportService.new } let!(:nare) { Fabricate(:account, username: 'nare', domain: 'թութ.հայ', locked: false, protocol: :activitypub, inbox_url: 'https://թութ.հայ/inbox') } @@ -200,7 +200,7 @@ RSpec.describe ImportService, type: :service do end end - context 'import bookmarks' do + context 'when importing bookmarks' do subject { ImportService.new } let(:csv) { attachment_fixture('bookmark-imports.txt') } diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb index 616a7aa204..8c99431fac 100644 --- a/spec/services/notify_service_spec.rb +++ b/spec/services/notify_service_spec.rb @@ -49,7 +49,7 @@ RSpec.describe NotifyService, type: :service do expect { subject }.to_not change(Notification, :count) end - context 'for direct messages' do + context 'with direct messages' do let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct)) } let(:type) { :mention } @@ -58,14 +58,14 @@ RSpec.describe NotifyService, type: :service do user.save end - context 'if recipient is supposed to be following sender' do + context 'when recipient is supposed to be following sender' do let(:enabled) { true } it 'does not notify' do expect { subject }.to_not change(Notification, :count) end - context 'if the message chain is initiated by recipient, but is not direct message' do + context 'when the message chain is initiated by recipient, but is not direct message' do let(:reply_to) { Fabricate(:status, account: recipient) } let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) } let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) } @@ -75,7 +75,7 @@ RSpec.describe NotifyService, type: :service do end end - context 'if the message chain is initiated by recipient, but without a mention to the sender, even if the sender sends multiple messages in a row' do + context 'when the message chain is initiated by recipient, but without a mention to the sender, even if the sender sends multiple messages in a row' do let(:reply_to) { Fabricate(:status, account: recipient) } let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) } let(:dummy_reply) { Fabricate(:status, account: sender, visibility: :direct, thread: reply_to) } @@ -86,7 +86,7 @@ RSpec.describe NotifyService, type: :service do end end - context 'if the message chain is initiated by the recipient with a mention to the sender' do + context 'when the message chain is initiated by the recipient with a mention to the sender' do let(:reply_to) { Fabricate(:status, account: recipient, visibility: :direct) } let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) } let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) } @@ -97,7 +97,7 @@ RSpec.describe NotifyService, type: :service do end end - context 'if recipient is NOT supposed to be following sender' do + context 'when recipient is NOT supposed to be following sender' do let(:enabled) { false } it 'does notify' do diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index adc45c60af..399800b2a6 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -33,10 +33,10 @@ RSpec.describe ProcessMentionsService, type: :service do end end - context 'resolving a mention to a remote account' do + context 'with resolving a mention to a remote account' do let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}", visibility: :public) } - context 'ActivityPub' do + context 'with ActivityPub' do context do let!(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } @@ -49,7 +49,7 @@ RSpec.describe ProcessMentionsService, type: :service do end end - context 'mentioning a user several times when not saving records' do + context 'when mentioning a user several times when not saving records' do let!(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct} @#{remote_user.acct} @#{remote_user.acct}", visibility: :public) } @@ -89,7 +89,7 @@ RSpec.describe ProcessMentionsService, type: :service do end end - context 'Temporarily-unreachable ActivityPub user' do + context 'with a Temporarily-unreachable ActivityPub user' do let!(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox', last_webfingered_at: nil) } before do diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb index 2ad6d30f6b..69500848d9 100644 --- a/spec/services/reblog_service_spec.rb +++ b/spec/services/reblog_service_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' RSpec.describe ReblogService, type: :service do let(:alice) { Fabricate(:account, username: 'alice') } - context 'creates a reblog with appropriate visibility' do + context 'when creates a reblog with appropriate visibility' do subject { ReblogService.new } let(:visibility) { :public } @@ -61,7 +61,7 @@ RSpec.describe ReblogService, type: :service do end end - context 'ActivityPub' do + context 'with ActivityPub' do subject { ReblogService.new } let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } diff --git a/spec/services/report_service_spec.rb b/spec/services/report_service_spec.rb index 452400f722..29207462a0 100644 --- a/spec/services/report_service_spec.rb +++ b/spec/services/report_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe ReportService, type: :service do let(:source_account) { Fabricate(:account) } - context 'for a remote account' do + context 'with a remote account' do let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } before do diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index 3ce1f7f2ba..ed22a8147a 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -11,11 +11,11 @@ RSpec.describe ResolveAccountService, type: :service do stub_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com').to_return(request_fixture('activitypub-webfinger.txt')) stub_request(:get, 'https://ap.example.com/users/foo').to_return(request_fixture('activitypub-actor.txt')) stub_request(:get, 'https://ap.example.com/users/foo.atom').to_return(request_fixture('activitypub-feed.txt')) - stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404) + stub_request(:get, %r{https://ap\.example\.com/users/foo/\w+}).to_return(status: 404) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:hoge@example.com').to_return(status: 410) end - context 'using skip_webfinger' do + context 'when using skip_webfinger' do context 'when account is known' do let!(:remote_account) { Fabricate(:account, username: 'foo', domain: 'ap.example.com', protocol: 'activitypub') } @@ -78,7 +78,7 @@ RSpec.describe ResolveAccountService, type: :service do end context 'when webfinger returns http gone' do - context 'for a previously known account' do + context 'with a previously known account' do before do Fabricate(:account, username: 'hoge', domain: 'example.com', last_webfingered_at: nil) allow(AccountDeletionWorker).to receive(:perform_async) @@ -94,7 +94,7 @@ RSpec.describe ResolveAccountService, type: :service do end end - context 'for a previously unknown account' do + context 'with a previously unknown account' do it 'returns nil' do expect(subject.call('hoge@example.com')).to be_nil end diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb index 3598311ee0..8d2af74173 100644 --- a/spec/services/resolve_url_service_spec.rb +++ b/spec/services/resolve_url_service_spec.rb @@ -30,7 +30,7 @@ describe ResolveURLService, type: :service do expect(subject.call(url)).to eq known_account end - context 'searching for a remote private status' do + context 'when searching for a remote private status' do let(:account) { Fabricate(:account) } let(:poster) { Fabricate(:account, domain: 'example.com') } let(:url) { 'https://example.com/@foo/42' } @@ -95,7 +95,7 @@ describe ResolveURLService, type: :service do end end - context 'searching for a local private status' do + context 'when searching for a local private status' do let(:account) { Fabricate(:account) } let(:poster) { Fabricate(:account) } let!(:status) { Fabricate(:status, account: poster, visibility: :private) } @@ -127,7 +127,7 @@ describe ResolveURLService, type: :service do end end - context 'searching for a link that redirects to a local public status' do + context 'when searching for a link that redirects to a local public status' do let(:account) { Fabricate(:account) } let(:poster) { Fabricate(:account) } let!(:status) { Fabricate(:status, account: poster, visibility: :public) } diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 1ad0efe0af..00f693dfab 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -23,7 +23,7 @@ describe SearchService, type: :service do @query = 'http://test.host/query' end - context 'that does not find anything' do + context 'when it does not find anything' do it 'returns the empty results' do service = double(call: nil) allow(ResolveURLService).to receive(:new).and_return(service) @@ -34,7 +34,7 @@ describe SearchService, type: :service do end end - context 'that finds an account' do + context 'when it finds an account' do it 'includes the account in the results' do account = Account.new service = double(call: account) @@ -46,7 +46,7 @@ describe SearchService, type: :service do end end - context 'that finds a status' do + context 'when it finds a status' do it 'includes the status in the results' do status = Status.new service = double(call: status) @@ -60,7 +60,7 @@ describe SearchService, type: :service do end describe 'with a non-url query' do - context 'that matches an account' do + context 'when it matches an account' do it 'includes the account in the results' do query = 'username' account = Account.new @@ -73,7 +73,7 @@ describe SearchService, type: :service do end end - context 'that matches a tag' do + context 'when it matches a tag' do it 'includes the tag in the results' do query = '#tag' tag = Tag.new diff --git a/spec/services/unallow_domain_service_spec.rb b/spec/services/unallow_domain_service_spec.rb index 48e310a9d1..fbc1d59592 100644 --- a/spec/services/unallow_domain_service_spec.rb +++ b/spec/services/unallow_domain_service_spec.rb @@ -12,7 +12,7 @@ RSpec.describe UnallowDomainService, type: :service do let!(:already_banned_account) { Fabricate(:account, username: 'badguy', domain: 'evil.org', suspended: true, silenced: true) } let!(:domain_allow) { Fabricate(:domain_allow, domain: 'evil.org') } - context 'in limited federation mode' do + context 'with limited federation mode' do before do allow(subject).to receive(:whitelist_mode?).and_return(true) end diff --git a/spec/services/verify_link_service_spec.rb b/spec/services/verify_link_service_spec.rb index ea9ccc3fc7..415788cb58 100644 --- a/spec/services/verify_link_service_spec.rb +++ b/spec/services/verify_link_service_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' RSpec.describe VerifyLinkService, type: :service do subject { described_class.new } - context 'given a local account' do + context 'when given a local account' do let(:account) { Fabricate(:account, username: 'alice') } let(:field) { Account::Field.new(account, 'name' => 'Website', 'value' => 'http://example.com') } @@ -129,7 +129,7 @@ RSpec.describe VerifyLinkService, type: :service do end end - context 'given a remote account' do + context 'when given a remote account' do let(:account) { Fabricate(:account, username: 'alice', domain: 'example.com', url: 'https://profile.example.com/alice') } let(:field) { Account::Field.new(account, 'name' => 'Website', 'value' => '<a href="http://example.com" rel="me"><span class="invisible">http://</span><span class="">example.com</span><span class="invisible"></span></a>') } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 25f3140026..dedb9719cd 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -38,7 +38,7 @@ RSpec.configure do |config| config.after :suite do gc_counter = 0 - FileUtils.rm_rf(Dir["#{Rails.root}/spec/test_files/"]) + FileUtils.rm_rf(Dir[Rails.root.join('spec', 'test_files')]) end config.after :each do diff --git a/spec/validators/disallowed_hashtags_validator_spec.rb b/spec/validators/disallowed_hashtags_validator_spec.rb index 896fd4fc5e..e98db38792 100644 --- a/spec/validators/disallowed_hashtags_validator_spec.rb +++ b/spec/validators/disallowed_hashtags_validator_spec.rb @@ -14,7 +14,7 @@ RSpec.describe DisallowedHashtagsValidator, type: :validator do let(:status) { double(errors: errors, local?: local, reblog?: reblog, text: disallowed_tags.map { |x| "##{x}" }.join(' ')) } let(:errors) { double(add: nil) } - context 'for a remote reblog' do + context 'with a remote reblog' do let(:local) { false } let(:reblog) { true } @@ -23,7 +23,7 @@ RSpec.describe DisallowedHashtagsValidator, type: :validator do end end - context 'for a local original status' do + context 'with a local original status' do let(:local) { true } let(:reblog) { false } diff --git a/spec/validators/email_mx_validator_spec.rb b/spec/validators/email_mx_validator_spec.rb index a11b8e01e0..d9703d81b1 100644 --- a/spec/validators/email_mx_validator_spec.rb +++ b/spec/validators/email_mx_validator_spec.rb @@ -6,7 +6,7 @@ describe EmailMxValidator do describe '#validate' do let(:user) { double(email: 'foo@example.com', sign_up_ip: '1.2.3.4', errors: double(add: nil)) } - context 'for an e-mail domain that is explicitly allowed' do + context 'with an e-mail domain that is explicitly allowed' do around do |block| tmp = Rails.configuration.x.email_domains_whitelist Rails.configuration.x.email_domains_whitelist = 'example.com' diff --git a/spec/validators/follow_limit_validator_spec.rb b/spec/validators/follow_limit_validator_spec.rb index 94ba0c47f8..7b9055a27f 100644 --- a/spec/validators/follow_limit_validator_spec.rb +++ b/spec/validators/follow_limit_validator_spec.rb @@ -18,7 +18,7 @@ RSpec.describe FollowLimitValidator, type: :validator do let(:_nil) { true } let(:local) { false } - context 'follow.account.nil? || !follow.account.local?' do + context 'with follow.account.nil? || !follow.account.local?' do let(:_nil) { true } it 'not calls errors.add' do @@ -26,11 +26,11 @@ RSpec.describe FollowLimitValidator, type: :validator do end end - context '!(follow.account.nil? || !follow.account.local?)' do + context 'with !(follow.account.nil? || !follow.account.local?)' do let(:_nil) { false } let(:local) { true } - context 'limit_reached?' do + context 'when limit_reached?' do let(:limit_reached) { true } it 'calls errors.add' do @@ -39,7 +39,7 @@ RSpec.describe FollowLimitValidator, type: :validator do end end - context '!limit_reached?' do + context 'with !limit_reached?' do let(:limit_reached) { false } it 'not calls errors.add' do diff --git a/spec/validators/poll_validator_spec.rb b/spec/validators/poll_validator_spec.rb index f3f4b12881..069a471619 100644 --- a/spec/validators/poll_validator_spec.rb +++ b/spec/validators/poll_validator_spec.rb @@ -18,7 +18,7 @@ RSpec.describe PollValidator, type: :validator do expect(errors).to_not have_received(:add) end - context 'expires just 5 min ago' do + context 'when expires is just 5 min ago' do let(:expires_at) { 5.minutes.from_now } it 'not calls errors add' do diff --git a/spec/validators/status_pin_validator_spec.rb b/spec/validators/status_pin_validator_spec.rb index d5bd0d1b83..00b89d702f 100644 --- a/spec/validators/status_pin_validator_spec.rb +++ b/spec/validators/status_pin_validator_spec.rb @@ -20,7 +20,7 @@ RSpec.describe StatusPinValidator, type: :validator do let(:reblog) { false } let(:count) { 0 } - context 'pin.status.reblog?' do + context 'when pin.status.reblog?' do let(:reblog) { true } it 'calls errors.add' do @@ -28,7 +28,7 @@ RSpec.describe StatusPinValidator, type: :validator do end end - context 'pin.account_id != pin.status.account_id' do + context 'when pin.account_id != pin.status.account_id' do let(:pin_account_id) { 1 } let(:status_account_id) { 2 } @@ -37,7 +37,7 @@ RSpec.describe StatusPinValidator, type: :validator do end end - context 'if pin.status.direct_visibility?' do + context 'when pin.status.direct_visibility?' do let(:visibility) { 'direct' } it 'calls errors.add' do @@ -45,7 +45,7 @@ RSpec.describe StatusPinValidator, type: :validator do end end - context 'pin.account.status_pins.count > 4 && pin.account.local?' do + context 'when pin.account.status_pins.count > 4 && pin.account.local?' do let(:count) { 5 } let(:local) { true } diff --git a/spec/validators/unreserved_username_validator_spec.rb b/spec/validators/unreserved_username_validator_spec.rb index 3c6f71c590..85bd7dcb6a 100644 --- a/spec/validators/unreserved_username_validator_spec.rb +++ b/spec/validators/unreserved_username_validator_spec.rb @@ -13,7 +13,7 @@ RSpec.describe UnreservedUsernameValidator, type: :validator do let(:account) { double(username: username, errors: errors) } let(:errors) { double(add: nil) } - context '@username.blank?' do + context 'when @username is blank?' do let(:username) { nil } it 'not calls errors.add' do @@ -21,10 +21,10 @@ RSpec.describe UnreservedUsernameValidator, type: :validator do end end - context '!@username.blank?' do + context 'when @username is not blank?' do let(:username) { 'f' } - context 'reserved_username?' do + context 'with reserved_username?' do let(:reserved_username) { true } it 'calls errors.add' do @@ -32,7 +32,7 @@ RSpec.describe UnreservedUsernameValidator, type: :validator do end end - context '!reserved_username?' do + context 'when username is not reserved' do let(:reserved_username) { false } it 'not calls errors.add' do diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb index 966261b505..a56ccd8e08 100644 --- a/spec/validators/url_validator_spec.rb +++ b/spec/validators/url_validator_spec.rb @@ -15,7 +15,7 @@ RSpec.describe URLValidator, type: :validator do let(:value) { '' } let(:attribute) { :foo } - context 'unless compliant?' do + context 'when not compliant?' do let(:compliant) { false } it 'calls errors.add' do @@ -23,7 +23,7 @@ RSpec.describe URLValidator, type: :validator do end end - context 'if compliant?' do + context 'when compliant?' do let(:compliant) { true } it 'not calls errors.add' do diff --git a/spec/workers/bulk_import_worker_spec.rb b/spec/workers/bulk_import_worker_spec.rb new file mode 100644 index 0000000000..91f51fbb42 --- /dev/null +++ b/spec/workers/bulk_import_worker_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe BulkImportWorker do + subject { described_class.new } + + let(:import) { Fabricate(:bulk_import, state: :scheduled) } + + describe '#perform' do + let(:service_double) { instance_double(BulkImportService, call: nil) } + + before do + allow(BulkImportService).to receive(:new).and_return(service_double) + end + + it 'changes the import\'s state as appropriate' do + expect { subject.perform(import.id) }.to change { import.reload.state.to_sym }.from(:scheduled).to(:in_progress) + end + + it 'calls BulkImportService' do + subject.perform(import.id) + expect(service_double).to have_received(:call).with(import) + end + end +end diff --git a/spec/workers/import/row_worker_spec.rb b/spec/workers/import/row_worker_spec.rb new file mode 100644 index 0000000000..0a71a838fc --- /dev/null +++ b/spec/workers/import/row_worker_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Import::RowWorker do + subject { described_class.new } + + let(:row) { Fabricate(:bulk_import_row, bulk_import: import) } + + describe '#perform' do + before do + allow(BulkImportRowService).to receive(:new).and_return(service_double) + end + + shared_examples 'clean failure' do + let(:service_double) { instance_double(BulkImportRowService, call: false) } + + it 'calls BulkImportRowService' do + subject.perform(row.id) + expect(service_double).to have_received(:call).with(row) + end + + it 'increases the number of processed items' do + expect { subject.perform(row.id) }.to(change { import.reload.processed_items }.by(+1)) + end + + it 'does not increase the number of imported items' do + expect { subject.perform(row.id) }.to_not(change { import.reload.imported_items }) + end + + it 'does not delete the row' do + subject.perform(row.id) + expect(BulkImportRow.exists?(row.id)).to be true + end + end + + shared_examples 'unclean failure' do + let(:service_double) { instance_double(BulkImportRowService) } + + before do + allow(service_double).to receive(:call) do + raise 'dummy error' + end + end + + it 'raises an error and does not change processed items count' do + expect { subject.perform(row.id) }.to raise_error(StandardError, 'dummy error').and(not_change { import.reload.processed_items }) + end + + it 'does not delete the row' do + expect { subject.perform(row.id) }.to raise_error(StandardError, 'dummy error').and(not_change { BulkImportRow.exists?(row.id) }) + end + end + + shared_examples 'clean success' do + let(:service_double) { instance_double(BulkImportRowService, call: true) } + + it 'calls BulkImportRowService' do + subject.perform(row.id) + expect(service_double).to have_received(:call).with(row) + end + + it 'increases the number of processed items' do + expect { subject.perform(row.id) }.to(change { import.reload.processed_items }.by(+1)) + end + + it 'increases the number of imported items' do + expect { subject.perform(row.id) }.to(change { import.reload.imported_items }.by(+1)) + end + + it 'deletes the row' do + expect { subject.perform(row.id) }.to change { BulkImportRow.exists?(row.id) }.from(true).to(false) + end + end + + context 'when there are multiple rows to process' do + let(:import) { Fabricate(:bulk_import, total_items: 2, processed_items: 0, imported_items: 0, state: :in_progress) } + + context 'with a clean failure' do + include_examples 'clean failure' + + it 'does not mark the import as finished' do + expect { subject.perform(row.id) }.to_not(change { import.reload.state.to_sym }) + end + end + + context 'with an unclean failure' do + include_examples 'unclean failure' + + it 'does not mark the import as finished' do + expect { subject.perform(row.id) }.to raise_error(StandardError).and(not_change { import.reload.state.to_sym }) + end + end + + context 'with a clean success' do + include_examples 'clean success' + + it 'does not mark the import as finished' do + expect { subject.perform(row.id) }.to_not(change { import.reload.state.to_sym }) + end + end + end + + context 'when this is the last row to process' do + let(:import) { Fabricate(:bulk_import, total_items: 2, processed_items: 1, imported_items: 0, state: :in_progress) } + + context 'with a clean failure' do + include_examples 'clean failure' + + it 'marks the import as finished' do + expect { subject.perform(row.id) }.to change { import.reload.state.to_sym }.from(:in_progress).to(:finished) + end + end + + # NOTE: sidekiq retry logic may be a bit too difficult to test, so leaving this blind spot for now + it_behaves_like 'unclean failure' + + context 'with a clean success' do + include_examples 'clean success' + + it 'marks the import as finished' do + expect { subject.perform(row.id) }.to change { import.reload.state.to_sym }.from(:in_progress).to(:finished) + end + end + end + end +end diff --git a/spec/workers/move_worker_spec.rb b/spec/workers/move_worker_spec.rb index e93060adb8..ac7bd506b6 100644 --- a/spec/workers/move_worker_spec.rb +++ b/spec/workers/move_worker_spec.rb @@ -5,22 +5,28 @@ require 'rails_helper' describe MoveWorker do subject { described_class.new } - let(:local_follower) { Fabricate(:account) } + let(:local_follower) { Fabricate(:account, domain: nil) } let(:blocking_account) { Fabricate(:account) } let(:muting_account) { Fabricate(:account) } - let(:source_account) { Fabricate(:account, protocol: :activitypub, domain: 'example.com') } - let(:target_account) { Fabricate(:account, protocol: :activitypub, domain: 'example.com') } + let(:source_account) { Fabricate(:account, protocol: :activitypub, domain: 'example.com', uri: 'https://example.org/a', inbox_url: 'https://example.org/a/inbox') } + let(:target_account) { Fabricate(:account, protocol: :activitypub, domain: 'example.com', uri: 'https://example.org/b', inbox_url: 'https://example.org/b/inbox') } let(:local_user) { Fabricate(:user) } let(:comment) { 'old note prior to move' } let!(:account_note) { Fabricate(:account_note, account: local_user.account, target_account: source_account, comment: comment) } + let(:list) { Fabricate(:list, account: local_follower) } let(:block_service) { double } before do + stub_request(:post, 'https://example.org/a/inbox').to_return(status: 200) + stub_request(:post, 'https://example.org/b/inbox').to_return(status: 200) + local_follower.follow!(source_account) blocking_account.block!(source_account) muting_account.mute!(source_account) + list.accounts << source_account + allow(BlockService).to receive(:new).and_return(block_service) allow(block_service).to receive(:call) end @@ -86,55 +92,100 @@ describe MoveWorker do end end - context 'both accounts are distant' do - describe 'perform' do - it 'calls UnfollowFollowWorker' do - expect_push_bulk_to_match(UnfollowFollowWorker, [[local_follower.id, source_account.id, target_account.id, false]]) - subject.perform(source_account.id, target_account.id) + shared_examples 'lists handling' do + it 'puts the new account on the list' do + subject.perform(source_account.id, target_account.id) + expect(list.accounts.include?(target_account)).to be true + end + + it 'does not create invalid list memberships' do + subject.perform(source_account.id, target_account.id) + expect(ListAccount.all).to all be_valid + end + end + + shared_examples 'common tests' do + include_examples 'user note handling' + include_examples 'block and mute handling' + include_examples 'followers count handling' + include_examples 'lists handling' + + context 'when a local user already follows both source and target' do + before do + local_follower.request_follow!(target_account) end include_examples 'user note handling' include_examples 'block and mute handling' include_examples 'followers count handling' + include_examples 'lists handling' + + context 'when the local user already has the target in a list' do + before do + list.accounts << target_account + end + + include_examples 'lists handling' + end end - end - context 'target account is local' do - let(:target_account) { Fabricate(:account) } - - describe 'perform' do - it 'calls UnfollowFollowWorker' do - expect_push_bulk_to_match(UnfollowFollowWorker, [[local_follower.id, source_account.id, target_account.id, true]]) - subject.perform(source_account.id, target_account.id) + context 'when a local follower already has a pending request to the target' do + before do + local_follower.follow!(target_account) end include_examples 'user note handling' include_examples 'block and mute handling' include_examples 'followers count handling' + include_examples 'lists handling' + + context 'when the local user already has the target in a list' do + before do + list.accounts << target_account + end + + include_examples 'lists handling' + end end end - context 'both target and source accounts are local' do - let(:target_account) { Fabricate(:account) } - let(:source_account) { Fabricate(:account) } + describe '#perform' do + context 'when both accounts are distant' do + it 'calls UnfollowFollowWorker' do + Sidekiq::Testing.fake! do + subject.perform(source_account.id, target_account.id) + expect(UnfollowFollowWorker).to have_enqueued_sidekiq_job(local_follower.id, source_account.id, target_account.id, false) + Sidekiq::Worker.drain_all + end + end + + include_examples 'common tests' + end + + context 'when target account is local' do + let(:target_account) { Fabricate(:account) } + + it 'calls UnfollowFollowWorker' do + Sidekiq::Testing.fake! do + subject.perform(source_account.id, target_account.id) + expect(UnfollowFollowWorker).to have_enqueued_sidekiq_job(local_follower.id, source_account.id, target_account.id, true) + Sidekiq::Worker.clear_all + end + end + + include_examples 'common tests' + end + + context 'when both target and source accounts are local' do + let(:target_account) { Fabricate(:account) } + let(:source_account) { Fabricate(:account) } - describe 'perform' do it 'calls makes local followers follow the target account' do subject.perform(source_account.id, target_account.id) expect(local_follower.following?(target_account)).to be true end - include_examples 'user note handling' - include_examples 'block and mute handling' - include_examples 'followers count handling' - - it 'does not fail when a local user is already following both accounts' do - double_follower = Fabricate(:account) - double_follower.follow!(source_account) - double_follower.follow!(target_account) - subject.perform(source_account.id, target_account.id) - expect(local_follower.following?(target_account)).to be true - end + include_examples 'common tests' it 'does not allow the moved account to follow themselves' do source_account.follow!(target_account) diff --git a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb index 436f2d93f3..8e747d04f5 100644 --- a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb +++ b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb @@ -75,8 +75,8 @@ describe Scheduler::AccountsStatusesCleanupScheduler do end end - describe '#get_budget' do - context 'on a single thread' do + describe '#compute_budget' do + context 'with a single thread' do let(:process_set_stub) { [{ 'concurrency' => 1, 'queues' => %w(push default) }] } it 'returns a low value' do @@ -84,7 +84,7 @@ describe Scheduler::AccountsStatusesCleanupScheduler do end end - context 'on a lot of threads' do + context 'with a lot of threads' do let(:process_set_stub) do [ { 'concurrency' => 2, 'queues' => %w(push default) }, @@ -130,6 +130,33 @@ describe Scheduler::AccountsStatusesCleanupScheduler do .and change { account3.statuses.count } .and change { account5.statuses.count } end + + context 'when given a big budget' do + let(:process_set_stub) { [{ 'concurrency' => 400, 'queues' => %w(push default) }] } + + before do + stub_const 'Scheduler::AccountsStatusesCleanupScheduler::MAX_BUDGET', 400 + end + + it 'correctly handles looping in a single run' do + expect(subject.compute_budget).to eq(400) + expect { subject.perform }.to change { Status.count }.by(-30) + end + end + + context 'when there is no work to be done' do + let(:process_set_stub) { [{ 'concurrency' => 400, 'queues' => %w(push default) }] } + + before do + stub_const 'Scheduler::AccountsStatusesCleanupScheduler::MAX_BUDGET', 400 + subject.perform + end + + it 'does not get stuck' do + expect(subject.compute_budget).to eq(400) + expect { subject.perform }.to_not change { Status.count } + end + end end end end diff --git a/yarn.lock b/yarn.lock index f12a891890..239c13a3e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,38 +31,38 @@ dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.4.tgz#457ffe647c480dff59c2be092fc3acf71195c87f" - integrity sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g== +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.5": + version "7.21.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.7.tgz#61caffb60776e49a57ba61a88f02bedd8714f6bc" + integrity sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA== -"@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.21.4", "@babel/core@^7.7.2": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.4.tgz#c6dc73242507b8e2a27fd13a9c1814f9fa34a659" - integrity sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA== +"@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.21.8", "@babel/core@^7.7.2": + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.8.tgz#2a8c7f0f53d60100ba4c32470ba0281c92aa9aa4" + integrity sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ== dependencies: "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-compilation-targets" "^7.21.4" - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helpers" "^7.21.0" - "@babel/parser" "^7.21.4" + "@babel/generator" "^7.21.5" + "@babel/helper-compilation-targets" "^7.21.5" + "@babel/helper-module-transforms" "^7.21.5" + "@babel/helpers" "^7.21.5" + "@babel/parser" "^7.21.8" "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.4" - "@babel/types" "^7.21.4" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.2" semver "^6.3.0" -"@babel/generator@^7.21.4", "@babel/generator@^7.7.2": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.4.tgz#64a94b7448989f421f919d5239ef553b37bb26bc" - integrity sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA== +"@babel/generator@^7.21.5", "@babel/generator@^7.7.2": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.5.tgz#c0c0e5449504c7b7de8236d99338c3e2a340745f" + integrity sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w== dependencies: - "@babel/types" "^7.21.4" + "@babel/types" "^7.21.5" "@jridgewell/gen-mapping" "^0.3.2" "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" @@ -90,12 +90,12 @@ "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/types" "^7.19.0" -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz#770cd1ce0889097ceacb99418ee6934ef0572656" - integrity sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg== +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz#631e6cc784c7b660417421349aac304c94115366" + integrity sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w== dependencies: - "@babel/compat-data" "^7.21.4" + "@babel/compat-data" "^7.21.5" "@babel/helper-validator-option" "^7.21.0" browserslist "^4.21.3" lru-cache "^5.1.1" @@ -162,6 +162,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-environment-visitor@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz#c769afefd41d171836f7cb63e295bedf689d48ba" + integrity sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ== + "@babel/helper-explode-assignable-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" @@ -213,19 +218,19 @@ dependencies: "@babel/types" "^7.21.4" -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.2": - version "7.21.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2" - integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ== +"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz#d937c82e9af68d31ab49039136a222b17ac0b420" + integrity sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw== dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-environment-visitor" "^7.21.5" + "@babel/helper-module-imports" "^7.21.4" + "@babel/helper-simple-access" "^7.21.5" "@babel/helper-split-export-declaration" "^7.18.6" "@babel/helper-validator-identifier" "^7.19.1" "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.2" - "@babel/types" "^7.21.2" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" @@ -234,10 +239,10 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.21.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56" + integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg== "@babel/helper-remap-async-to-generator@^7.18.9": version "7.18.9" @@ -261,12 +266,12 @@ "@babel/traverse" "^7.20.7" "@babel/types" "^7.20.7" -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== +"@babel/helper-simple-access@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz#d697a7971a5c39eac32c7e63c0921c06c8a249ee" + integrity sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg== dependencies: - "@babel/types" "^7.20.2" + "@babel/types" "^7.21.5" "@babel/helper-skip-transparent-expression-wrappers@^7.20.0": version "7.20.0" @@ -282,10 +287,10 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd" + integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w== "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" @@ -307,14 +312,14 @@ "@babel/traverse" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/helpers@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.0.tgz#9dd184fb5599862037917cdc9eecb84577dc4e7e" - integrity sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA== +"@babel/helpers@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.5.tgz#5bac66e084d7a4d2d9696bdf0175a93f7fb63c08" + integrity sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA== dependencies: "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.0" - "@babel/types" "^7.21.0" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" "@babel/highlight@^7.18.6": version "7.18.6" @@ -325,10 +330,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" - integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8": + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" + integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -524,7 +529,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.19.0" -"@babel/plugin-syntax-import-meta@^7.8.3": +"@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== @@ -615,12 +620,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-arrow-functions@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551" - integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ== +"@babel/plugin-transform-arrow-functions@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.21.5.tgz#9bb42a53de447936a57ba256fbf537fc312b6929" + integrity sha512-wb1mhwGOCaXHDTcsRYMKF9e5bbMgqwxtqa2Y1ifH96dXJPwbuLX9qHy3clhrxVqgMz7nyNXs8VkxdH8UBcjKqA== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/plugin-transform-async-to-generator@^7.20.7": version "7.20.7" @@ -660,12 +665,12 @@ "@babel/helper-split-export-declaration" "^7.18.6" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz#704cc2fd155d1c996551db8276d55b9d46e4d0aa" - integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ== +"@babel/plugin-transform-computed-properties@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.21.5.tgz#3a2d8bb771cd2ef1cd736435f6552fe502e11b44" + integrity sha512-TR653Ki3pAwxBxUe8srfF3e4Pe3FTA46uaNHYyQwIoM4oWKSoOZiDNyHJ0oIoDIUPSRQbQG7jzgVBX3FPVne1Q== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/template" "^7.20.7" "@babel/plugin-transform-destructuring@^7.21.3": @@ -698,12 +703,12 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-for-of@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz#964108c9988de1a60b4be2354a7d7e245f36e86e" - integrity sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ== +"@babel/plugin-transform-for-of@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.5.tgz#e890032b535f5a2e237a18535f56a9fdaa7b83fc" + integrity sha512-nYWpjKW/7j/I/mZkGVgHJXh4bA1sfdFnJoOXwJuj4m3Q2EraO/8ZyrkCau9P5tbHQk01RMSt6KYLCsW7730SXQ== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/plugin-transform-function-name@^7.18.9": version "7.18.9" @@ -736,14 +741,14 @@ "@babel/helper-module-transforms" "^7.20.11" "@babel/helper-plugin-utils" "^7.20.2" -"@babel/plugin-transform-modules-commonjs@^7.21.2": - version "7.21.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz#6ff5070e71e3192ef2b7e39820a06fb78e3058e7" - integrity sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA== +"@babel/plugin-transform-modules-commonjs@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.5.tgz#d69fb947eed51af91de82e4708f676864e5e47bc" + integrity sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ== dependencies: - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-module-transforms" "^7.21.5" + "@babel/helper-plugin-utils" "^7.21.5" + "@babel/helper-simple-access" "^7.21.5" "@babel/plugin-transform-modules-systemjs@^7.20.11": version "7.20.11" @@ -841,12 +846,12 @@ "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-regenerator@^7.20.5": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz#57cda588c7ffb7f4f8483cc83bdcea02a907f04d" - integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ== +"@babel/plugin-transform-regenerator@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz#576c62f9923f94bcb1c855adc53561fd7913724e" + integrity sha512-ZoYBKDb6LyMi5yCsByQ5jmXsHAQDDYeexT1Szvlmui+lADvfSecr5Dxd/PkrTC3pAD182Fcju1VQkB4oCp9M+w== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" regenerator-transform "^0.15.1" "@babel/plugin-transform-reserved-words@^7.18.6": @@ -914,12 +919,12 @@ "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-typescript" "^7.20.0" -"@babel/plugin-transform-unicode-escapes@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246" - integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== +"@babel/plugin-transform-unicode-escapes@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.21.5.tgz#1e55ed6195259b0e9061d81f5ef45a9b009fb7f2" + integrity sha512-LYm/gTOwZqsYohlvFUe/8Tujz75LqqVC2w+2qPHLR+WyWHGCZPN1KBpJCJn+4Bk4gOkQy/IXKIge6az5MqwlOg== dependencies: - "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/plugin-transform-unicode-regex@^7.18.6": version "7.18.6" @@ -929,14 +934,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.21.4.tgz#a952482e634a8dd8271a3fe5459a16eb10739c58" - integrity sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw== +"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.21.5.tgz#db2089d99efd2297716f018aeead815ac3decffb" + integrity sha512-wH00QnTTldTbf/IefEVyChtRdw5RJvODT/Vb4Vcxq1AZvtXj6T0YeX0cAcXhI6/BdGuiP3GcNIL4OQbI2DVNxg== dependencies: - "@babel/compat-data" "^7.21.4" - "@babel/helper-compilation-targets" "^7.21.4" - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/compat-data" "^7.21.5" + "@babel/helper-compilation-targets" "^7.21.5" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/helper-validator-option" "^7.21.0" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.20.7" @@ -961,6 +966,7 @@ "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" "@babel/plugin-syntax-import-assertions" "^7.20.0" + "@babel/plugin-syntax-import-meta" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" @@ -970,22 +976,22 @@ "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.20.7" + "@babel/plugin-transform-arrow-functions" "^7.21.5" "@babel/plugin-transform-async-to-generator" "^7.20.7" "@babel/plugin-transform-block-scoped-functions" "^7.18.6" "@babel/plugin-transform-block-scoping" "^7.21.0" "@babel/plugin-transform-classes" "^7.21.0" - "@babel/plugin-transform-computed-properties" "^7.20.7" + "@babel/plugin-transform-computed-properties" "^7.21.5" "@babel/plugin-transform-destructuring" "^7.21.3" "@babel/plugin-transform-dotall-regex" "^7.18.6" "@babel/plugin-transform-duplicate-keys" "^7.18.9" "@babel/plugin-transform-exponentiation-operator" "^7.18.6" - "@babel/plugin-transform-for-of" "^7.21.0" + "@babel/plugin-transform-for-of" "^7.21.5" "@babel/plugin-transform-function-name" "^7.18.9" "@babel/plugin-transform-literals" "^7.18.9" "@babel/plugin-transform-member-expression-literals" "^7.18.6" "@babel/plugin-transform-modules-amd" "^7.20.11" - "@babel/plugin-transform-modules-commonjs" "^7.21.2" + "@babel/plugin-transform-modules-commonjs" "^7.21.5" "@babel/plugin-transform-modules-systemjs" "^7.20.11" "@babel/plugin-transform-modules-umd" "^7.18.6" "@babel/plugin-transform-named-capturing-groups-regex" "^7.20.5" @@ -993,17 +999,17 @@ "@babel/plugin-transform-object-super" "^7.18.6" "@babel/plugin-transform-parameters" "^7.21.3" "@babel/plugin-transform-property-literals" "^7.18.6" - "@babel/plugin-transform-regenerator" "^7.20.5" + "@babel/plugin-transform-regenerator" "^7.21.5" "@babel/plugin-transform-reserved-words" "^7.18.6" "@babel/plugin-transform-shorthand-properties" "^7.18.6" "@babel/plugin-transform-spread" "^7.20.7" "@babel/plugin-transform-sticky-regex" "^7.18.6" "@babel/plugin-transform-template-literals" "^7.18.9" "@babel/plugin-transform-typeof-symbol" "^7.18.9" - "@babel/plugin-transform-unicode-escapes" "^7.18.10" + "@babel/plugin-transform-unicode-escapes" "^7.21.5" "@babel/plugin-transform-unicode-regex" "^7.18.6" "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.21.4" + "@babel/types" "^7.21.5" babel-plugin-polyfill-corejs2 "^0.3.3" babel-plugin-polyfill-corejs3 "^0.6.0" babel-plugin-polyfill-regenerator "^0.4.1" @@ -1033,15 +1039,15 @@ "@babel/plugin-transform-react-jsx-development" "^7.18.6" "@babel/plugin-transform-react-pure-annotations" "^7.18.6" -"@babel/preset-typescript@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.4.tgz#b913ac8e6aa8932e47c21b01b4368d8aa239a529" - integrity sha512-sMLNWY37TCdRH/bJ6ZeeOH1nPuanED7Ai9Y/vH31IPqalioJ6ZNFUWONsakhv4r4n+I6gm5lmoE0olkgib/j/A== +"@babel/preset-typescript@^7.21.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.5.tgz#68292c884b0e26070b4d66b202072d391358395f" + integrity sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA== dependencies: - "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-plugin-utils" "^7.21.5" "@babel/helper-validator-option" "^7.21.0" "@babel/plugin-syntax-jsx" "^7.21.4" - "@babel/plugin-transform-modules-commonjs" "^7.21.2" + "@babel/plugin-transform-modules-commonjs" "^7.21.5" "@babel/plugin-transform-typescript" "^7.21.3" "@babel/regjsgen@^0.8.0": @@ -1064,10 +1070,10 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" - integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200" + integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== dependencies: regenerator-runtime "^0.13.11" @@ -1080,28 +1086,28 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" -"@babel/traverse@^7.18.10", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4", "@babel/traverse@^7.7.2": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.4.tgz#a836aca7b116634e97a6ed99976236b3282c9d36" - integrity sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q== +"@babel/traverse@^7.18.10", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.5", "@babel/traverse@^7.7.2": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.5.tgz#ad22361d352a5154b498299d523cf72998a4b133" + integrity sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw== dependencies: "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-environment-visitor" "^7.18.9" + "@babel/generator" "^7.21.5" + "@babel/helper-environment-visitor" "^7.21.5" "@babel/helper-function-name" "^7.21.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.21.4" - "@babel/types" "^7.21.4" + "@babel/parser" "^7.21.5" + "@babel/types" "^7.21.5" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4" - integrity sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA== +"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6" + integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q== dependencies: - "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-string-parser" "^7.21.5" "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" @@ -1271,29 +1277,29 @@ dependencies: "@floating-ui/core" "^1.0.1" -"@formatjs/ecma402-abstract@1.14.3": - version "1.14.3" - resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz#6428f243538a11126180d121ce8d4b2f17465738" - integrity sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg== +"@formatjs/ecma402-abstract@1.15.0": + version "1.15.0" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.15.0.tgz#0a285a5dc69889e15d53803bd5036272e23e5a18" + integrity sha512-7bAYAv0w4AIao9DNg0avfOLTCPE9woAgs6SpXuMq11IN3A+l+cq8ghczwqSZBM11myvPSJA7vLn72q0rJ0QK6Q== dependencies: "@formatjs/intl-localematcher" "0.2.32" tslib "^2.4.0" -"@formatjs/icu-messageformat-parser@2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.3.1.tgz#953080ea5c053bc73bdf55d0a524a3c3c133ae6b" - integrity sha512-knF2AkAKN4Upv4oIiKY4Wd/dLH68TNMPgV/tJMu/T6FP9aQwbv8fpj7U3lkyniPaNVxvia56Gxax8MKOjtxLSQ== +"@formatjs/icu-messageformat-parser@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.4.0.tgz#e165f3594c68416ce15f63793768251de2a85f88" + integrity sha512-6Dh5Z/gp4F/HovXXu/vmd0If5NbYLB5dZrmhWVNb+BOGOEU3wt7Z/83KY1dtd7IDhAnYHasbmKE1RbTE0J+3hw== dependencies: - "@formatjs/ecma402-abstract" "1.14.3" - "@formatjs/icu-skeleton-parser" "1.3.18" + "@formatjs/ecma402-abstract" "1.15.0" + "@formatjs/icu-skeleton-parser" "1.4.0" tslib "^2.4.0" -"@formatjs/icu-skeleton-parser@1.3.18": - version "1.3.18" - resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz#7aed3d60e718c8ad6b0e64820be44daa1e29eeeb" - integrity sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg== +"@formatjs/icu-skeleton-parser@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.4.0.tgz#96342eca7c4eef7a309875569e5da973db3465e6" + integrity sha512-Qq347VM616rVLkvN6QsKJELazRyNlbCiN47LdH0Mc5U7E2xV0vatiVhGqd3KFgbc055BvtnUXR7XX60dCGFuWg== dependencies: - "@formatjs/ecma402-abstract" "1.14.3" + "@formatjs/ecma402-abstract" "1.15.0" tslib "^2.4.0" "@formatjs/intl-localematcher@0.2.32": @@ -1315,12 +1321,12 @@ resolved "https://registry.yarnpkg.com/@formatjs/intl-utils/-/intl-utils-2.2.5.tgz#eaafd94df3d102ee13e54e80f992a33868a6b1e8" integrity sha512-p7gcmazKROteL4IECCp03Qrs790fZ8tbemUAjQu0+K0AaAlK49rI1SIFFq3LzDUAqXIshV95JJhRe/yXxkal5g== -"@formatjs/ts-transformer@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-3.13.0.tgz#61185278fb153d61e56fabbeed6d4fc8a0ee3af5" - integrity sha512-TshsXkt2loK2GWFJFYTrlNThfCd4ubcEpokl9FWzGoR5f5e2FOxDPs69nTqw+7jodlKtx4VaTSfpNMtPvD9ZfQ== +"@formatjs/ts-transformer@3.13.1": + version "3.13.1" + resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-3.13.1.tgz#37aa4992aa50740f808f1f888f112b8addb617c7" + integrity sha512-U5BuLqFx5wre5Q0NrZhBh7itMJZISYuZGoj8HdN/UO1EKaTL2HQjE1G040GjTpY0k+AAyaHK3b8WPqgHLvIaIQ== dependencies: - "@formatjs/icu-messageformat-parser" "2.3.1" + "@formatjs/icu-messageformat-parser" "2.4.0" "@types/json-stable-stringify" "^1.0.32" "@types/node" "14 || 16 || 17" chalk "^4.0.0" @@ -1721,6 +1727,16 @@ resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.4.tgz#af85eb080f6934580e4d3b58046026b6c2b18717" integrity sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng== +"@reduxjs/toolkit@^1.9.5": + version "1.9.5" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4" + integrity sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ== + dependencies: + immer "^9.0.21" + redux "^4.2.1" + redux-thunk "^2.4.2" + reselect "^4.1.8" + "@restart/hooks@^0.4.7": version "0.4.7" resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.7.tgz#d79ca6472c01ce04389fc73d4a79af1b5e33cd39" @@ -2459,15 +2475,15 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz#9b09ee1541bff1d2cebdcb87e7ce4a4003acde08" - integrity sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg== +"@typescript-eslint/eslint-plugin@^5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz#684a2ce7182f3b4dac342eef7caa1c2bae476abd" + integrity sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.1" - "@typescript-eslint/type-utils" "5.59.1" - "@typescript-eslint/utils" "5.59.1" + "@typescript-eslint/scope-manager" "5.59.2" + "@typescript-eslint/type-utils" "5.59.2" + "@typescript-eslint/utils" "5.59.2" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" @@ -2475,98 +2491,98 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.1.tgz#73c2c12127c5c1182d2e5b71a8fa2a85d215cbb4" - integrity sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g== +"@typescript-eslint/parser@^5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.2.tgz#c2c443247901d95865b9f77332d9eee7c55655e8" + integrity sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ== dependencies: - "@typescript-eslint/scope-manager" "5.59.1" - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/typescript-estree" "5.59.1" + "@typescript-eslint/scope-manager" "5.59.2" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/typescript-estree" "5.59.2" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz#8a20222719cebc5198618a5d44113705b51fd7fe" - integrity sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA== +"@typescript-eslint/scope-manager@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz#f699fe936ee4e2c996d14f0fdd3a7da5ba7b9a4c" + integrity sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA== dependencies: - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/visitor-keys" "5.59.1" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/visitor-keys" "5.59.2" -"@typescript-eslint/type-utils@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz#63981d61684fd24eda2f9f08c0a47ecb000a2111" - integrity sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw== +"@typescript-eslint/type-utils@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz#0729c237503604cd9a7084b5af04c496c9a4cdcf" + integrity sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ== dependencies: - "@typescript-eslint/typescript-estree" "5.59.1" - "@typescript-eslint/utils" "5.59.1" + "@typescript-eslint/typescript-estree" "5.59.2" + "@typescript-eslint/utils" "5.59.2" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5" - integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA== +"@typescript-eslint/types@5.59.0": + version "5.59.0" + 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.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.1.tgz#03f3fedd1c044cb336ebc34cc7855f121991f41d" - integrity sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg== +"@typescript-eslint/types@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.2.tgz#b511d2b9847fe277c5cb002a2318bd329ef4f655" + integrity sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w== -"@typescript-eslint/typescript-estree@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d" - integrity sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ== +"@typescript-eslint/typescript-estree@5.59.0": + version "5.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.0.tgz#8869156ee1dcfc5a95be3ed0e2809969ea28e965" + integrity sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg== dependencies: - "@typescript-eslint/types" "5.45.0" - "@typescript-eslint/visitor-keys" "5.45.0" + "@typescript-eslint/types" "5.59.0" + "@typescript-eslint/visitor-keys" "5.59.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz#4aa546d27fd0d477c618f0ca00b483f0ec84c43c" - integrity sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA== +"@typescript-eslint/typescript-estree@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz#6e2fabd3ba01db5d69df44e0b654c0b051fe9936" + integrity sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q== dependencies: - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/visitor-keys" "5.59.1" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/visitor-keys" "5.59.2" 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.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.1.tgz#d89fc758ad23d2157cfae53f0b429bdf15db9473" - integrity sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA== +"@typescript-eslint/utils@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.2.tgz#0c45178124d10cc986115885688db6abc37939f4" + integrity sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ== 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.1" - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/typescript-estree" "5.59.1" + "@typescript-eslint/scope-manager" "5.59.2" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/typescript-estree" "5.59.2" eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.45.0": - version "5.45.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528" - integrity sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg== +"@typescript-eslint/visitor-keys@5.59.0": + version "5.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.0.tgz#a59913f2bf0baeb61b5cfcb6135d3926c3854365" + integrity sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA== dependencies: - "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/types" "5.59.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz#0d96c36efb6560d7fb8eb85de10442c10d8f6058" - integrity sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA== +"@typescript-eslint/visitor-keys@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz#37a419dc2723a3eacbf722512b86d6caf7d3b750" + integrity sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig== dependencies: - "@typescript-eslint/types" "5.59.1" + "@typescript-eslint/types" "5.59.2" eslint-visitor-keys "^3.3.0" "@webassemblyjs/ast@1.9.0": @@ -3163,10 +3179,10 @@ axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece" integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg== -axios@^1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.6.tgz#1ace9a9fb994314b5f6327960918406fa92c6646" - integrity sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg== +axios@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" + integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== dependencies: follow-redirects "^1.15.0" form-data "^4.0.0" @@ -4309,14 +4325,14 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -cssnano-preset-default@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-6.0.0.tgz#058726536bdc18711c01b1d328766cbc5691cf71" - integrity sha512-BDxlaFzObRDXUiCCBQUNQcI+f1/aX2mgoNtXGjV6PG64POcHoDUoX+LgMWw+Q4609QhxwkcSnS65YFs42RA6qQ== +cssnano-preset-default@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-6.0.1.tgz#2a93247140d214ddb9f46bc6a3562fa9177fe301" + integrity sha512-7VzyFZ5zEB1+l1nToKyrRkuaJIx0zi/1npjvZfbBwbtNTzhLtlvYraK/7/uqmX2Wb2aQtd983uuGw79jAjLSuQ== dependencies: css-declaration-sorter "^6.3.1" cssnano-utils "^4.0.0" - postcss-calc "^8.2.3" + postcss-calc "^9.0.0" postcss-colormin "^6.0.0" postcss-convert-values "^6.0.0" postcss-discard-comments "^6.0.0" @@ -4324,7 +4340,7 @@ cssnano-preset-default@^6.0.0: postcss-discard-empty "^6.0.0" postcss-discard-overridden "^6.0.0" postcss-merge-longhand "^6.0.0" - postcss-merge-rules "^6.0.0" + postcss-merge-rules "^6.0.1" postcss-minify-font-values "^6.0.0" postcss-minify-gradients "^6.0.0" postcss-minify-params "^6.0.0" @@ -4349,12 +4365,12 @@ cssnano-utils@^4.0.0: resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-4.0.0.tgz#d1da885ec04003ab19505ff0e62e029708d36b08" integrity sha512-Z39TLP+1E0KUcd7LGyF4qMfu8ZufI0rDzhdyAMsa/8UyNUU8wpS0fhdBxbQbv32r64ea00h4878gommRVg2BHw== -cssnano@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-6.0.0.tgz#53f4cb81101cccba0809fad779f006b5d44925ee" - integrity sha512-RGlcbzGhzEBCHuQe3k+Udyj5M00z0pm9S+VurHXFEOXxH+y0sVrJH2sMzoyz2d8N1EScazg+DVvmgyx0lurwwA== +cssnano@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-6.0.1.tgz#87c38c4cd47049c735ab756d7e77ac3ca855c008" + integrity sha512-fVO1JdJ0LSdIGJq68eIxOqFpIJrZqXUsBt8fkrBcztCQqAjQD51OhZp7tc0ImcbwXD4k7ny84QTV90nZhmqbkg== dependencies: - cssnano-preset-default "^6.0.0" + cssnano-preset-default "^6.0.1" lilconfig "^2.1.0" csso@^5.0.5: @@ -5035,20 +5051,20 @@ eslint-module-utils@^2.7.4: dependencies: debug "^3.2.7" -eslint-plugin-formatjs@^4.9.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-formatjs/-/eslint-plugin-formatjs-4.10.0.tgz#05da23f75b4ce507c90df93ff07be2b6e70ffbc5" - integrity sha512-YvNF72NVMkIevgJrX5xTkIj4eBCeiweM2/61ppP2eEni3FP4pDXy9UFsOhtxJVISTBH0UEAqz3xhRjqi1q+qag== +eslint-plugin-formatjs@^4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-formatjs/-/eslint-plugin-formatjs-4.10.1.tgz#c67184ac54188dcad84d6541e6b5467248ab6550" + integrity sha512-sD3GGdfDQqiaW8TgbUD4lrUR+raIgusPzW+0v+iN36QzkHvpg5L0UZGFQE9GWtgnWfXAndb57UpgB0i/CF2G7w== dependencies: - "@formatjs/icu-messageformat-parser" "2.3.1" - "@formatjs/ts-transformer" "3.13.0" + "@formatjs/icu-messageformat-parser" "2.4.0" + "@formatjs/ts-transformer" "3.13.1" "@types/eslint" "7 || 8" "@types/picomatch" "^2.3.0" - "@typescript-eslint/typescript-estree" "5.45.0" + "@typescript-eslint/typescript-estree" "5.59.0" emoji-regex "^10.2.1" - magic-string "^0.29.0" + magic-string "^0.30.0" picomatch "^2.3.1" - tslib "2.4.0" + tslib "2.5.0" typescript "^4.7 || 5" unicode-emoji-utils "^1.1.1" @@ -6296,6 +6312,11 @@ ignore@^5.2.0, ignore@^5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immer@^9.0.21: + version "9.0.21" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" + integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== + immutable@^3.8.2: version "3.8.2" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" @@ -7409,10 +7430,10 @@ jsdom@^20.0.0: ws "^8.11.0" xml-name-validator "^4.0.0" -jsdom@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.1.1.tgz#ab796361e3f6c01bcfaeda1fea3c06197ac9d8ae" - integrity sha512-Jjgdmw48RKcdAIQyUD1UdBh2ecH7VqwaXPN3ehoZN6MqgVbMn+lRm1aAT1AsdJRAJpwfa4IpwgzySn61h2qu3w== +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== dependencies: abab "^2.0.6" acorn "^8.8.2" @@ -7427,7 +7448,7 @@ jsdom@^21.1.1: http-proxy-agent "^5.0.0" https-proxy-agent "^5.0.1" is-potential-custom-element-name "^1.0.1" - nwsapi "^2.2.2" + nwsapi "^2.2.4" parse5 "^7.1.2" rrweb-cssom "^0.6.0" saxes "^6.0.0" @@ -7810,10 +7831,10 @@ magic-string@^0.25.0, magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.8" -magic-string@^0.29.0: - version "0.29.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.29.0.tgz#f034f79f8c43dba4ae1730ffb5e8c4e084b16cf3" - integrity sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q== +magic-string@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529" + integrity sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ== dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" @@ -8364,10 +8385,10 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" -nwsapi@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" - integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== +nwsapi@^2.2.2, nwsapi@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.4.tgz#fd59d5e904e8e1f03c25a7d5a15cfa16c714a1e5" + integrity sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g== object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" @@ -8920,12 +8941,12 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -postcss-calc@^8.2.3: - version "8.2.4" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" - integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== +postcss-calc@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-9.0.0.tgz#cd9b2b155e57c823687eb67c9afcbe97c98ecaa4" + integrity sha512-B9BNW/SVh4SMJfoCQ6D9h1Wo7Yjqks7UdbiARJ16J5TIsQn5NEqwMF5joSgOYb26oJPUR5Uv3fCQ/4PvmZWeJQ== dependencies: - postcss-selector-parser "^6.0.9" + postcss-selector-parser "^6.0.11" postcss-value-parser "^4.2.0" postcss-colormin@^6.0.0: @@ -8990,10 +9011,10 @@ postcss-merge-longhand@^6.0.0: postcss-value-parser "^4.2.0" stylehacks "^6.0.0" -postcss-merge-rules@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-6.0.0.tgz#0d95bc73541156b8b4e763bd0de2c3f9d0ecf013" - integrity sha512-rCXkklftzEkniyv3f4mRCQzxD6oE4Quyh61uyWTUbCJ26Pv2hoz+fivJSsSBWxDBeScR4fKCfF3HHTcD7Ybqnw== +postcss-merge-rules@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-6.0.1.tgz#39f165746404e646c0f5c510222ccde4824a86aa" + integrity sha512-a4tlmJIQo9SCjcfiCcCMg/ZCEe0XTkl/xK0XHBs955GWg9xDX3NwP9pwZ78QUOWB8/0XCjZeJn98Dae0zg6AAw== dependencies: browserslist "^4.21.4" caniuse-api "^3.0.0" @@ -9160,10 +9181,10 @@ postcss-scss@^4.0.6: resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.6.tgz#5d62a574b950a6ae12f2aa89b60d63d9e4432bfd" integrity sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ== -postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: - version "6.0.11" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" - integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== +postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.12, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5: + version "6.0.12" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.12.tgz#2efae5ffab3c8bfb2b7fbf0c426e3bca616c4abb" + integrity sha512-NdxGCAZdRrwVI1sy59+Wzrh+pMMHxapGnpfenDVlMEXoOcvt4pGE0JLK9YY2F5dLxcFYA/YbVQKhcGU+FtSYQg== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" @@ -9188,7 +9209,7 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.2.15, postcss@^8.4.22, postcss@^8.4.23: +postcss@^8.2.15, postcss@^8.4.23: version "8.4.23" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== @@ -9612,10 +9633,10 @@ react-router@^4.3.1: prop-types "^15.6.1" warning "^4.0.1" -react-select@*, react-select@^5.7.2: - version "5.7.2" - resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.7.2.tgz#ccd40071b9429277983bf15526e7a5773a060e09" - integrity sha512-cTlJkQ8YjV6T/js8wW0owTzht0hHGABh29vjLscY4HfZGkv7hc3FFTmRp9NzY/Ib1uQ36GieAKEjxpHdpCFpcA== +react-select@*, react-select@^5.7.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.7.3.tgz#fa0dc9a23cad6ff3871ad3829f6083a4b54961a2" + integrity sha512-z8i3NCuFFWL3w27xq92rBkVI2onT0jzIIPe480HlBjXJ3b5o6Q+Clp4ydyeKrj9DZZ3lrjawwLC5NGl0FSvUDg== dependencies: "@babel/runtime" "^7.12.0" "@emotion/cache" "^11.4.0" @@ -10949,10 +10970,10 @@ stylelint-scss@^4.6.0: postcss-selector-parser "^6.0.11" postcss-value-parser "^4.2.0" -stylelint@^15.6.0: - version "15.6.0" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.6.0.tgz#1d76176dd8b6307bc4645e428ad18ddd15edbafc" - integrity sha512-Cqzpc8tvJm77KaM8qUbhpJ/UYK55Ia0whQXj4b9IId9dlPICO7J8Lyo15SZWiHxKjlvy3p5FQor/3n6i8ignXg== +stylelint@^15.6.1: + version "15.6.1" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.6.1.tgz#e4cd33a3af88587b99a5d1328aedd8c298b6dc81" + integrity sha512-d8icFBlVl93Elf3Z5ABQNOCe4nx69is3D/NZhDLAie1eyYnpxfeKe7pCfqzT5W4F8vxHCLSDfV8nKNJzogvV2Q== dependencies: "@csstools/css-parser-algorithms" "^2.1.1" "@csstools/css-tokenizer" "^2.1.1" @@ -10981,11 +11002,11 @@ stylelint@^15.6.0: micromatch "^4.0.5" normalize-path "^3.0.0" picocolors "^1.0.0" - postcss "^8.4.22" + postcss "^8.4.23" postcss-media-query-parser "^0.2.3" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^6.0.0" - postcss-selector-parser "^6.0.11" + postcss-selector-parser "^6.0.12" postcss-value-parser "^4.2.0" resolve-from "^5.0.0" string-width "^4.2.3" @@ -10995,7 +11016,7 @@ stylelint@^15.6.0: svg-tags "^1.0.0" table "^6.8.1" v8-compile-cache "^2.3.0" - write-file-atomic "^5.0.0" + write-file-atomic "^5.0.1" stylis@4.0.13: version "4.0.13" @@ -11316,26 +11337,16 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" - integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== - -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^1.9.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== - -tslib@^2.1.0, tslib@^2.4.0: +tslib@2.5.0, tslib@^2.1.0, tslib@^2.4.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== +tslib@^1.8.1, tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -12256,13 +12267,13 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" -write-file-atomic@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.0.tgz#54303f117e109bf3d540261125c8ea5a7320fab0" - integrity sha512-R7NYMnHSlV42K54lwY9lvW6MnSm1HSJqZL3xiSgi9E7//FYaI74r2G0rd+/X6VAMkHEdzxQaU5HUOXWUz5kA/w== +write-file-atomic@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" + integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== dependencies: imurmurhash "^0.1.4" - signal-exit "^3.0.7" + signal-exit "^4.0.1" ws@^6.2.1: version "6.2.1" @@ -12360,10 +12371,10 @@ yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@^17.3.1, yargs@^17.7.1: - version "17.7.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967" - integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw== +yargs@^17.3.1, yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" escalade "^3.1.1"