diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 425b86a6bb..04ac9560ca 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,16 +1,14 @@ -# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.1, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.1-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.1-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster -ARG VARIANT=3.1-bullseye -FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT} +# For details, see https://github.com/devcontainers/images/tree/main/src/ruby +FROM mcr.microsoft.com/devcontainers/ruby:0-3.2-bullseye # Install Rails # RUN gem install rails webdrivers # Default value to allow debug server to serve content over GitHub Codespace's port forwarding service # The value is a comma-separated list of allowed domains -ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev" +ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev" -# [Choice] Node.js version: lts/*, 18, 16, 14 -ARG NODE_VERSION="lts/*" +ARG NODE_VERSION="16" RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" # [Optional] Uncomment this section to install additional OS packages. @@ -22,3 +20,5 @@ RUN gem install foreman # [Optional] Uncomment this line to install global node packages. RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1 + +COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6ac6993ee9..d628fd1bd1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,30 +1,13 @@ +// For more details, see https://aka.ms/devcontainer.json. { "name": "Mastodon", "dockerComposeFile": "docker-compose.yml", "service": "app", - "workspaceFolder": "/mastodon", - - // Configure tool-specific properties. - "customizations": { - // Configure properties specific to VS Code. - "vscode": { - // Set *default* container specific settings.json values on container create. - "settings": {}, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "EditorConfig.EditorConfig", - "dbaeumer.vscode-eslint", - "rebornix.Ruby", - "webben.browserslist" - ] - } - }, + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Features to add to the dev container. More info: https://containers.dev/features. "features": { - "ghcr.io/devcontainers/features/sshd:1": { - "version": "latest" - } + "ghcr.io/devcontainers/features/sshd:1": {} }, // Use 'forwardPorts' to make a list of ports inside the container available locally. @@ -33,7 +16,16 @@ // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": ".devcontainer/post-create.sh", + "waitFor": "postCreateCommand", - // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": {}, + // Add the IDs of extensions you want installed when the container is created. + "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] + } + } } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 95f401379c..19f9c0b5b3 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,15 +5,8 @@ services: build: context: . dockerfile: Dockerfile - args: - # Update 'VARIANT' to pick a version of Ruby: 3, 3.1, 3.0, 2, 2.7, 2.6 - # Append -bullseye or -buster to pin to an OS version. - # Use -bullseye variants on local arm64/Apple Silicon. - VARIANT: '3.0-bullseye' - # Optional Node.js version to install - NODE_VERSION: '16' volumes: - - ..:/mastodon:cached + - ../..:/workspaces:cached environment: RAILS_ENV: development NODE_ENV: development @@ -33,7 +26,6 @@ services: networks: - external_network - internal_network - user: vscode db: image: postgres:14-alpine diff --git a/.devcontainer/welcome-message.txt b/.devcontainer/welcome-message.txt new file mode 100644 index 0000000000..488cf92857 --- /dev/null +++ b/.devcontainer/welcome-message.txt @@ -0,0 +1,8 @@ +👋 Welcome to "Mastodon" in GitHub Codespaces! + +🛠️ Your environment is fully setup with all the required software. + +🔍 To explore VS Code to its fullest, search using the Command Palette (Cmd/Ctrl + Shift + P or F1). + +📝 Edit away, run your app as usual, and we'll automatically make it available for you to access. + diff --git a/.rubocop.yml b/.rubocop.yml index 27d778edfb..b4387a4611 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -19,7 +19,6 @@ AllCops: NewCops: enable Exclude: - db/schema.rb - - 'app/views/**/*' - 'config/**/*' - 'bin/*' - 'Rakefile' @@ -97,6 +96,10 @@ Rails/Exit: - 'lib/mastodon/cli_helper.rb' - 'lib/cli.rb' +RSpec/FilePath: + CustomTransform: + DeepL: deepl + RSpec/NotToNot: EnforcedStyle: to_not @@ -123,3 +126,6 @@ Style/TrailingCommaInArrayLiteral: Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: 'comma' + +Style/SymbolArray: + Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 22e1a99c12..b53f655bd0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -2235,134 +2235,3 @@ Style/SlicingWithRange: - 'lib/active_record/batches.rb' - 'lib/mastodon/premailer_webpack_strategy.rb' - 'lib/tasks/repo.rake' - -# Offense count: 272 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MinSize. -# SupportedStyles: percent, brackets -Style/SymbolArray: - Exclude: - - 'app/controllers/accounts_controller.rb' - - 'app/controllers/activitypub/replies_controller.rb' - - 'app/controllers/admin/accounts_controller.rb' - - 'app/controllers/admin/announcements_controller.rb' - - 'app/controllers/admin/domain_blocks_controller.rb' - - 'app/controllers/admin/email_domain_blocks_controller.rb' - - 'app/controllers/admin/relationships_controller.rb' - - 'app/controllers/admin/relays_controller.rb' - - 'app/controllers/admin/roles_controller.rb' - - 'app/controllers/admin/rules_controller.rb' - - 'app/controllers/admin/statuses_controller.rb' - - 'app/controllers/admin/trends/statuses_controller.rb' - - 'app/controllers/admin/warning_presets_controller.rb' - - 'app/controllers/admin/webhooks_controller.rb' - - 'app/controllers/api/v1/accounts/credentials_controller.rb' - - 'app/controllers/api/v1/accounts_controller.rb' - - 'app/controllers/api/v1/admin/accounts_controller.rb' - - 'app/controllers/api/v1/admin/canonical_email_blocks_controller.rb' - - 'app/controllers/api/v1/admin/domain_allows_controller.rb' - - 'app/controllers/api/v1/admin/domain_blocks_controller.rb' - - 'app/controllers/api/v1/admin/email_domain_blocks_controller.rb' - - 'app/controllers/api/v1/admin/ip_blocks_controller.rb' - - 'app/controllers/api/v1/admin/reports_controller.rb' - - 'app/controllers/api/v1/crypto/deliveries_controller.rb' - - 'app/controllers/api/v1/crypto/keys/claims_controller.rb' - - 'app/controllers/api/v1/crypto/keys/uploads_controller.rb' - - 'app/controllers/api/v1/featured_tags_controller.rb' - - 'app/controllers/api/v1/filters_controller.rb' - - 'app/controllers/api/v1/lists_controller.rb' - - 'app/controllers/api/v1/notifications_controller.rb' - - 'app/controllers/api/v1/push/subscriptions_controller.rb' - - 'app/controllers/api/v1/scheduled_statuses_controller.rb' - - 'app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb' - - 'app/controllers/api/v1/statuses_controller.rb' - - 'app/controllers/api/v2/filters/keywords_controller.rb' - - 'app/controllers/api/v2/filters/statuses_controller.rb' - - 'app/controllers/api/v2/filters_controller.rb' - - 'app/controllers/api/web/push_subscriptions_controller.rb' - - 'app/controllers/application_controller.rb' - - 'app/controllers/auth/registrations_controller.rb' - - 'app/controllers/filters_controller.rb' - - 'app/controllers/settings/applications_controller.rb' - - 'app/controllers/settings/featured_tags_controller.rb' - - 'app/controllers/settings/profiles_controller.rb' - - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb' - - 'app/controllers/statuses_controller.rb' - - 'app/lib/feed_manager.rb' - - 'app/models/account.rb' - - 'app/models/account_filter.rb' - - 'app/models/admin/status_filter.rb' - - 'app/models/announcement.rb' - - 'app/models/concerns/ldap_authenticable.rb' - - 'app/models/concerns/status_threading_concern.rb' - - 'app/models/custom_filter.rb' - - 'app/models/domain_block.rb' - - 'app/models/import.rb' - - 'app/models/list.rb' - - 'app/models/media_attachment.rb' - - 'app/models/preview_card.rb' - - 'app/models/relay.rb' - - 'app/models/report.rb' - - 'app/models/site_upload.rb' - - 'app/models/status.rb' - - 'app/serializers/initial_state_serializer.rb' - - 'app/serializers/rest/notification_serializer.rb' - - 'db/migrate/20160220174730_create_accounts.rb' - - 'db/migrate/20160221003621_create_follows.rb' - - 'db/migrate/20160223171800_create_favourites.rb' - - 'db/migrate/20160224223247_create_mentions.rb' - - 'db/migrate/20160314164231_add_owner_to_application.rb' - - 'db/migrate/20160316103650_add_missing_indices.rb' - - 'db/migrate/20160926213048_remove_owner_from_application.rb' - - 'db/migrate/20161003145426_create_blocks.rb' - - 'db/migrate/20161006213403_rails_settings_migration.rb' - - 'db/migrate/20161105130633_create_statuses_tags_join_table.rb' - - 'db/migrate/20161119211120_create_notifications.rb' - - 'db/migrate/20161128103007_create_subscriptions.rb' - - 'db/migrate/20161222204147_create_follow_requests.rb' - - 'db/migrate/20170112154826_migrate_settings.rb' - - 'db/migrate/20170301222600_create_mutes.rb' - - 'db/migrate/20170406215816_add_notifications_and_favourites_indices.rb' - - 'db/migrate/20170424003227_create_account_domain_blocks.rb' - - 'db/migrate/20170427011934_re_add_owner_to_application.rb' - - 'db/migrate/20170507141759_optimize_index_subscriptions.rb' - - 'db/migrate/20170508230434_create_conversation_mutes.rb' - - 'db/migrate/20170720000000_add_index_favourites_on_account_id_and_id.rb' - - 'db/migrate/20170823162448_create_status_pins.rb' - - 'db/migrate/20170901142658_create_join_table_preview_cards_statuses.rb' - - 'db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb' - - 'db/migrate/20170917153509_create_custom_emojis.rb' - - 'db/migrate/20170918125918_ids_to_bigints.rb' - - 'db/migrate/20171116161857_create_list_accounts.rb' - - 'db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb' - - 'db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb' - - 'db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb' - - 'db/migrate/20171129172043_add_index_on_stream_entries.rb' - - 'db/migrate/20171226094803_more_faster_index_on_notifications.rb' - - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb' - - 'db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb' - - 'db/migrate/20180808175627_create_account_pins.rb' - - 'db/migrate/20180831171112_create_bookmarks.rb' - - 'db/migrate/20180929222014_create_account_conversations.rb' - - 'db/migrate/20181007025445_create_pghero_space_stats.rb' - - 'db/migrate/20181203003808_create_accounts_tags_join_table.rb' - - 'db/migrate/20190316190352_create_account_identity_proofs.rb' - - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' - - 'db/migrate/20190820003045_update_statuses_index.rb' - - 'db/migrate/20190823221802_add_local_index_to_statuses.rb' - - 'db/migrate/20190904222339_create_markers.rb' - - 'db/migrate/20200113125135_create_announcement_mutes.rb' - - 'db/migrate/20200114113335_create_announcement_reactions.rb' - - 'db/migrate/20200119112504_add_public_index_to_statuses.rb' - - 'db/migrate/20200628133322_create_account_notes.rb' - - 'db/migrate/20200917222316_add_index_notifications_on_type.rb' - - 'db/migrate/20210425135952_add_index_on_media_attachments_account_id_status_id.rb' - - 'db/migrate/20220714171049_create_tag_follows.rb' - - 'db/migrate/20221021055441_add_index_featured_tags_on_account_id_and_tag_id.rb' - - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' - - 'db/post_migrate/20200917222734_remove_index_notifications_on_account_activity.rb' - - 'spec/controllers/api/v1/streaming_controller_spec.rb' - - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - - 'spec/fabricators/notification_fabricator.rb' - - 'spec/models/public_feed_spec.rb' diff --git a/Gemfile b/Gemfile index d9678a8892..ec7e62579b 100644 --- a/Gemfile +++ b/Gemfile @@ -104,8 +104,6 @@ group :development, :test do gem 'fabrication', '~> 2.30' gem 'fuubar', '~> 2.5' gem 'i18n-tasks', '~> 1.0', require: false - gem 'pry-byebug', '~> 3.10' - gem 'pry-rails', '~> 0.3' gem 'rspec-rails', '~> 6.0' gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false @@ -119,7 +117,6 @@ end group :test do gem 'capybara', '~> 3.38' - gem 'climate_control', '~> 0.2' gem 'faker', '~> 3.1' gem 'json-schema', '~> 3.0' gem 'rack-test', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 22cb86df82..ce3b39fd77 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -144,7 +144,7 @@ GEM bootsnap (1.16.0) msgpack (~> 1.2) brakeman (5.4.0) - browser (4.2.0) + browser (5.3.1) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) @@ -155,7 +155,6 @@ GEM bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) - byebug (11.1.3) capistrano (3.17.2) airbrussh (>= 1.0.0) i18n @@ -499,14 +498,6 @@ GEM net-smtp premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) - pry (0.14.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.10.1) - byebug (~> 11.0) - pry (>= 0.13, < 0.15) - pry-rails (0.3.9) - pry (>= 0.10.4) public_suffix (5.0.1) puma (6.1.0) nio4r (~> 2.0) @@ -569,7 +560,7 @@ GEM rdf-normalize (0.5.1) rdf (~> 3.2) redcarpet (3.6.0) - redis (4.5.1) + redis (4.8.1) redis-namespace (1.10.0) redis (>= 4) redlock (1.3.2) @@ -794,7 +785,6 @@ DEPENDENCIES capybara (~> 3.38) charlock_holmes (~> 0.7.7) chewy (~> 7.2) - climate_control (~> 0.2) cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby @@ -853,8 +843,6 @@ DEPENDENCIES posix-spawn premailer-rails private_address_check (~> 0.5) - pry-byebug (~> 3.10) - pry-rails (~> 0.3) public_suffix (~> 5.0) puma (~> 6.1) pundit (~> 2.3) diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index d40770726c..52cf1e0c18 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -20,6 +20,8 @@ class RelationshipsController < ApplicationController @form.save rescue ActionController::ParameterMissing # Do nothing + rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound + flash[:alert] = I18n.t('relationships.follow_failure') if action_from_button == 'follow' ensure redirect_to relationships_path(filter_params) end @@ -61,8 +63,8 @@ class RelationshipsController < ApplicationController 'unfollow' elsif params[:remove_from_followers] 'remove_from_followers' - elsif params[:block_domains] - 'block_domains' + elsif params[:block_domains] || params[:remove_domains_from_followers] + 'remove_domains_from_followers' end end diff --git a/app/javascript/flavours/glitch/components/status_content.jsx b/app/javascript/flavours/glitch/components/status_content.jsx index 790a5c6591..f217320659 100644 --- a/app/javascript/flavours/glitch/components/status_content.jsx +++ b/app/javascript/flavours/glitch/components/status_content.jsx @@ -5,7 +5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl'; import Permalink from './permalink'; import classnames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; -import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'flavours/glitch/initial_state'; +import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state'; import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; const textMatchesTarget = (text, origin, host) => { @@ -315,7 +315,7 @@ class StatusContent extends React.PureComponent { } = this.props; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; - const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language'); + const renderTranslate = this.props.onTranslate && status.get('translatable'); const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') }; diff --git a/app/javascript/flavours/glitch/features/ui/components/header.jsx b/app/javascript/flavours/glitch/features/ui/components/header.jsx index d9ad949611..5aa045fee9 100644 --- a/app/javascript/flavours/glitch/features/ui/components/header.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/header.jsx @@ -23,8 +23,8 @@ const mapDispatchToProps = (dispatch) => ({ }, }); -export default @connect(null, mapDispatchToProps) -@withRouter +export default @withRouter +@connect(null, mapDispatchToProps) class Header extends React.PureComponent { static contextTypes = { diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx index 3b46c6eec0..5a296435f6 100644 --- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx @@ -78,8 +78,8 @@ class NavigationPanel extends React.Component { {signedIn && ( - + diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index c4b249db81..03be4ff6cf 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -140,7 +140,6 @@ export const unfollowModal = getMeta('unfollow_modal'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); export const version = getMeta('version'); -export const translationEnabled = getMeta('translation_enabled'); export const languages = initialState?.languages; export const statusPageUrl = getMeta('status_page_url'); diff --git a/app/javascript/flavours/glitch/styles/components/compose_form.scss b/app/javascript/flavours/glitch/styles/components/compose_form.scss index aa2cc664ee..1c2e0aeb4d 100644 --- a/app/javascript/flavours/glitch/styles/components/compose_form.scss +++ b/app/javascript/flavours/glitch/styles/components/compose_form.scss @@ -164,8 +164,7 @@ white-space: pre-wrap; p, - pre, - blockquote { + pre { margin-bottom: 20px; white-space: pre-wrap; @@ -174,79 +173,6 @@ } } - h1, - h2, - h3, - h4, - h5 { - margin-top: 20px; - margin-bottom: 20px; - } - - h1, - h2 { - font-weight: 700; - font-size: 18px; - } - - h2 { - font-size: 16px; - } - - h3, - h4, - h5 { - font-weight: 500; - } - - blockquote { - padding-left: 10px; - border-left: 3px solid $inverted-text-color; - color: $inverted-text-color; - white-space: normal; - - p:last-child { - margin-bottom: 0; - } - } - - b, - strong { - font-weight: 700; - } - - em, - i { - font-style: italic; - } - - sub { - font-size: smaller; - vertical-align: sub; - } - - sup { - font-size: smaller; - vertical-align: super; - } - - ul, - ol { - margin-left: 1em; - - p { - margin: 0; - } - } - - ul { - list-style-type: disc; - } - - ol { - list-style-type: decimal; - } - a { color: $lighter-text-color; text-decoration: none; diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index 7f94b25e9f..1a7dfe9aed 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -68,8 +68,7 @@ } p, - pre, - blockquote { + pre { margin-bottom: 20px; white-space: pre-wrap; unicode-bidi: plaintext; @@ -79,89 +78,6 @@ } } - .status__content__text, - .e-content { - overflow: hidden; - - & > ul, - & > ol { - margin-bottom: 20px; - } - - h1, - h2, - h3, - h4, - h5 { - margin-top: 20px; - margin-bottom: 20px; - } - - h1, - h2 { - font-weight: 700; - font-size: 1.2em; - } - - h2 { - font-size: 1.1em; - } - - h3, - h4, - h5 { - font-weight: 500; - } - - blockquote { - padding-left: 10px; - border-left: 3px solid $darker-text-color; - color: $darker-text-color; - white-space: normal; - - p:last-child { - margin-bottom: 0; - } - } - - b, - strong { - font-weight: 700; - } - - em, - i { - font-style: italic; - } - - sub { - font-size: smaller; - vertical-align: sub; - } - - sup { - font-size: smaller; - vertical-align: super; - } - - ul, - ol { - margin-left: 2em; - - p { - margin: 0; - } - } - - ul { - list-style-type: disc; - } - - ol { - list-style-type: decimal; - } - } - a { color: $secondary-text-color; text-decoration: none; diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss index fbb02c7884..1cb913c8b8 100644 --- a/app/javascript/flavours/glitch/styles/index.scss +++ b/app/javascript/flavours/glitch/styles/index.scss @@ -21,3 +21,4 @@ @import 'accessibility'; @import 'rtl'; @import 'dashboard'; +@import 'rich_text'; diff --git a/app/javascript/flavours/glitch/styles/rich_text.scss b/app/javascript/flavours/glitch/styles/rich_text.scss new file mode 100644 index 0000000000..e608183534 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/rich_text.scss @@ -0,0 +1,99 @@ +.status__content__text, +.e-content, +.reply-indicator__content { + pre, + blockquote { + margin-bottom: 20px; + white-space: pre-wrap; + unicode-bidi: plaintext; + + &:last-child { + margin-bottom: 0; + } + } + + blockquote { + padding-left: 10px; + border-left: 3px solid $darker-text-color; + color: $darker-text-color; + white-space: normal; + + p:last-child { + margin-bottom: 0; + } + } + + & > ul, + & > ol { + margin-bottom: 20px; + } + + h1, + h2, + h3, + h4, + h5 { + margin-top: 20px; + margin-bottom: 20px; + } + + h1, + h2 { + font-weight: 700; + font-size: 1.2em; + } + + h2 { + font-size: 1.1em; + } + + h3, + h4, + h5 { + font-weight: 500; + } + + b, + strong { + font-weight: 700; + } + + em, + i { + font-style: italic; + } + + sub { + font-size: smaller; + vertical-align: sub; + } + + sup { + font-size: smaller; + vertical-align: super; + } + + ul, + ol { + margin-left: 2em; + + p { + margin: 0; + } + } + + ul { + list-style-type: disc; + } + + ol { + list-style-type: decimal; + } +} + +.reply-indicator__content { + blockquote { + border-left-color: $inverted-text-color; + color: $inverted-text-color; + } +} diff --git a/app/javascript/mastodon/components/column_back_button.jsx b/app/javascript/mastodon/components/column_back_button.jsx index 5bbf11652b..5c5226b7ea 100644 --- a/app/javascript/mastodon/components/column_back_button.jsx +++ b/app/javascript/mastodon/components/column_back_button.jsx @@ -15,10 +15,10 @@ export default class ColumnBackButton extends React.PureComponent { }; handleClick = () => { - if (window.history && window.history.length === 1) { - this.context.router.history.push('/'); - } else { + if (window.history && window.history.state) { this.context.router.history.goBack(); + } else { + this.context.router.history.push('/'); } }; diff --git a/app/javascript/mastodon/components/column_header.jsx b/app/javascript/mastodon/components/column_header.jsx index 38f6ad60ff..9ba783d903 100644 --- a/app/javascript/mastodon/components/column_header.jsx +++ b/app/javascript/mastodon/components/column_header.jsx @@ -43,14 +43,6 @@ class ColumnHeader extends React.PureComponent { animating: false, }; - historyBack = () => { - if (window.history && window.history.length === 1) { - this.context.router.history.push('/'); - } else { - this.context.router.history.goBack(); - } - }; - handleToggleClick = (e) => { e.stopPropagation(); this.setState({ collapsed: !this.state.collapsed, animating: true }); @@ -69,7 +61,11 @@ class ColumnHeader extends React.PureComponent { }; handleBackClick = () => { - this.historyBack(); + if (window.history && window.history.state) { + this.context.router.history.goBack(); + } else { + this.context.router.history.push('/'); + } }; handleTransitionEnd = () => { diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index a1c38171f2..f9c9fe0791 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -6,7 +6,7 @@ import { Link } from 'react-router-dom'; import classnames from 'classnames'; import PollContainer from 'mastodon/containers/poll_container'; import Icon from 'mastodon/components/icon'; -import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state'; +import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state'; const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) @@ -220,7 +220,7 @@ class StatusContent extends React.PureComponent { const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const renderReadMore = this.props.onClick && status.get('collapsed'); - const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language'); + const renderTranslate = this.props.onTranslate && status.get('translatable'); const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') }; diff --git a/app/javascript/mastodon/features/ui/components/header.jsx b/app/javascript/mastodon/features/ui/components/header.jsx index 1384bebda0..92adc47a9c 100644 --- a/app/javascript/mastodon/features/ui/components/header.jsx +++ b/app/javascript/mastodon/features/ui/components/header.jsx @@ -22,8 +22,8 @@ const mapDispatchToProps = (dispatch) => ({ }, }); -export default @connect(null, mapDispatchToProps) -@withRouter +export default @withRouter +@connect(null, mapDispatchToProps) class Header extends React.PureComponent { static contextTypes = { diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx index 9a9309be05..755b19349d 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx @@ -82,8 +82,8 @@ class NavigationPanel extends React.Component { {signedIn && ( - + diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 4f0ea04504..2dd59f95d4 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -474,10 +474,10 @@ class UI extends React.PureComponent { }; handleHotkeyBack = () => { - if (window.history && window.history.length === 1) { - this.context.router.history.push('/'); - } else { + if (window.history && window.history.state) { this.context.router.history.goBack(); + } else { + this.context.router.history.push('/'); } }; diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index c9c3a7647b..cab7f1f6b2 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -80,7 +80,6 @@ * @property {boolean} use_blurhash * @property {boolean=} use_pending_items * @property {string} version - * @property {boolean} translation_enabled */ /** @@ -132,7 +131,6 @@ export const unfollowModal = getMeta('unfollow_modal'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); export const version = getMeta('version'); -export const translationEnabled = getMeta('translation_enabled'); export const languages = initialState?.languages; export const statusPageUrl = getMeta('status_page_url'); diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 81a040108e..1b2969c234 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -23,3 +23,4 @@ @import 'mastodon/dashboard'; @import 'mastodon/rtl'; @import 'mastodon/accessibility'; +@import 'mastodon/rich_text'; diff --git a/app/javascript/styles/mastodon/rich_text.scss b/app/javascript/styles/mastodon/rich_text.scss new file mode 100644 index 0000000000..35901984b4 --- /dev/null +++ b/app/javascript/styles/mastodon/rich_text.scss @@ -0,0 +1,64 @@ +.status__content__text, +.e-content, +.reply-indicator__content { + pre, + blockquote { + margin-bottom: 20px; + white-space: pre-wrap; + unicode-bidi: plaintext; + + &:last-child { + margin-bottom: 0; + } + } + + blockquote { + padding-left: 10px; + border-left: 3px solid $darker-text-color; + color: $darker-text-color; + white-space: normal; + + p:last-child { + margin-bottom: 0; + } + } + + & > ul, + & > ol { + margin-bottom: 20px; + } + + b, + strong { + font-weight: 700; + } + + em, + i { + font-style: italic; + } + + ul, + ol { + margin-left: 2em; + + p { + margin: 0; + } + } + + ul { + list-style-type: disc; + } + + ol { + list-style-type: decimal; + } +} + +.reply-indicator__content { + blockquote { + border-left-color: $inverted-text-color; + color: $inverted-text-color; + } +} diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index be5b68b3ff..4ce888fc99 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -322,27 +322,27 @@ class FeedManager def clean_feeds!(type, ids) reblogged_id_sets = {} - redis.pipelined do + redis.pipelined do |pipeline| ids.each do |feed_id| - redis.del(key(type, feed_id)) reblog_key = key(type, feed_id, 'reblogs') # We collect a future for this: we don't block while getting # it, but we can iterate over it later. - reblogged_id_sets[feed_id] = redis.zrange(reblog_key, 0, -1) - redis.del(reblog_key) + reblogged_id_sets[feed_id] = pipeline.zrange(reblog_key, 0, -1) + pipeline.del(key(type, feed_id), reblog_key) end end # Remove all of the reblog tracking keys we just removed the # references to. - redis.pipelined do - reblogged_id_sets.each do |feed_id, future| - future.value.each do |reblogged_id| - reblog_set_key = key(type, feed_id, "reblogs:#{reblogged_id}") - redis.del(reblog_set_key) - end + keys_to_delete = reblogged_id_sets.flat_map do |feed_id, future| + future.value.map do |reblogged_id| + key(type, feed_id, "reblogs:#{reblogged_id}") end end + + redis.del(keys_to_delete) unless keys_to_delete.empty? + + nil end private diff --git a/app/lib/translation_service.rb b/app/lib/translation_service.rb index 285f309393..5ff93674a4 100644 --- a/app/lib/translation_service.rb +++ b/app/lib/translation_service.rb @@ -21,6 +21,10 @@ class TranslationService ENV['DEEPL_API_KEY'].present? || ENV['LIBRE_TRANSLATE_ENDPOINT'].present? end + def supported?(_source_language, _target_language) + false + end + def translate(_text, _source_language, _target_language) raise NotImplementedError end diff --git a/app/lib/translation_service/deepl.rb b/app/lib/translation_service/deepl.rb index 151d33d909..deff95a1db 100644 --- a/app/lib/translation_service/deepl.rb +++ b/app/lib/translation_service/deepl.rb @@ -11,33 +11,53 @@ class TranslationService::DeepL < TranslationService end def translate(text, source_language, target_language) - request(text, source_language, target_language).perform do |res| + form = { text: text, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' } + request(:post, '/v2/translate', form: form) do |res| + transform_response(res.body_with_limit) + end + end + + def supported?(source_language, target_language) + source_language.in?(languages('source')) && target_language.in?(languages('target')) + end + + private + + def languages(type) + Rails.cache.fetch("translation_service/deepl/languages/#{type}", expires_in: 7.days, race_condition_ttl: 1.minute) do + request(:get, "/v2/languages?type=#{type}") do |res| + # In DeepL, EN and PT are deprecated in favor of EN-GB/EN-US and PT-BR/PT-PT, so + # they are supported but not returned by the API. + extra = type == 'source' ? [nil] : %w(en pt) + languages = Oj.load(res.body_with_limit).map { |language| language['language'].downcase } + + languages + extra + end + end + end + + def request(verb, path, **options) + req = Request.new(verb, "#{base_url}#{path}", **options) + req.add_headers(Authorization: "DeepL-Auth-Key #{@api_key}") + req.perform do |res| case res.code when 429 raise TooManyRequestsError when 456 raise QuotaExceededError when 200...300 - transform_response(res.body_with_limit) + yield res else raise UnexpectedResponseError end end end - private - - def request(text, source_language, target_language) - req = Request.new(:post, endpoint_url, form: { text: text, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' }) - req.add_headers(Authorization: "DeepL-Auth-Key #{@api_key}") - req - end - - def endpoint_url + def base_url if @plan == 'free' - 'https://api-free.deepl.com/v2/translate' + 'https://api-free.deepl.com' else - 'https://api.deepl.com/v2/translate' + 'https://api.deepl.com' end end diff --git a/app/lib/translation_service/libre_translate.rb b/app/lib/translation_service/libre_translate.rb index 4ebe21e454..743e4d77f7 100644 --- a/app/lib/translation_service/libre_translate.rb +++ b/app/lib/translation_service/libre_translate.rb @@ -9,29 +9,45 @@ class TranslationService::LibreTranslate < TranslationService end def translate(text, source_language, target_language) - request(text, source_language, target_language).perform do |res| + body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key) + request(:post, '/translate', body: body) do |res| + transform_response(res.body_with_limit, source_language) + end + end + + def supported?(source_language, target_language) + languages.key?(source_language) && languages[source_language].include?(target_language) + end + + private + + def languages + Rails.cache.fetch('translation_service/libre_translate/languages', expires_in: 7.days, race_condition_ttl: 1.minute) do + request(:get, '/languages') do |res| + languages = Oj.load(res.body_with_limit).to_h { |language| [language['code'], language['targets']] } + languages[nil] = languages.values.flatten.uniq + languages + end + end + end + + def request(verb, path, **options) + req = Request.new(verb, "#{@base_url}#{path}", allow_local: true, **options) + req.add_headers('Content-Type': 'application/json') + req.perform do |res| case res.code when 429 raise TooManyRequestsError when 403 raise QuotaExceededError when 200...300 - transform_response(res.body_with_limit, source_language) + yield res else raise UnexpectedResponseError end end end - private - - def request(text, source_language, target_language) - body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key) - req = Request.new(:post, "#{@base_url}/translate", body: body, allow_local: true) - req.add_headers('Content-Type': 'application/json') - req - end - def transform_response(str, source_language) json = Oj.load(str, mode: :strict) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 73b623576c..35f0b5fee1 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -7,9 +7,17 @@ class ApplicationMailer < ActionMailer::Base helper :instance helper :formatting + after_action :set_autoreply_headers! + protected def locale_for_account(account, &block) I18n.with_locale(account.user_locale || I18n.default_locale, &block) end + + def set_autoreply_headers! + headers['Precedence'] = 'list' + headers['X-Auto-Response-Suppress'] = 'All' + headers['Auto-Submitted'] = 'auto-generated' + end end diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb index b0aa5be6f1..41eae215b4 100644 --- a/app/models/concerns/omniauthable.rb +++ b/app/models/concerns/omniauthable.rb @@ -61,7 +61,7 @@ module Omniauthable user.account.avatar_remote_url = nil end - user.skip_confirmation! if email_is_verified + user.confirm! if email_is_verified user.save! user end diff --git a/app/models/follow_recommendation_suppression.rb b/app/models/follow_recommendation_suppression.rb index a9dbbfc18f..e261a2fe35 100644 --- a/app/models/follow_recommendation_suppression.rb +++ b/app/models/follow_recommendation_suppression.rb @@ -20,9 +20,9 @@ class FollowRecommendationSuppression < ApplicationRecord private def remove_follow_recommendations - redis.pipelined do + redis.pipelined do |pipeline| I18n.available_locales.each do |locale| - redis.zrem("follow_recommendations:#{locale}", account_id) + pipeline.zrem("follow_recommendations:#{locale}", account_id) end end end diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index 473622edf4..6a05f8163a 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -17,8 +17,8 @@ class Form::AccountBatch unfollow! when 'remove_from_followers' remove_from_followers! - when 'block_domains' - block_domains! + when 'remove_domains_from_followers' + remove_domains_from_followers! when 'approve' approve! when 'reject' @@ -35,9 +35,15 @@ class Form::AccountBatch private def follow! + error = nil + accounts.each do |target_account| FollowService.new.call(current_account, target_account) + rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound => e + error ||= e end + + raise error if error.present? end def unfollow! @@ -50,10 +56,8 @@ class Form::AccountBatch RemoveFromFollowersService.new.call(current_account, account_ids) end - def block_domains! - AfterAccountDomainBlockWorker.push_bulk(account_domains) do |domain| - [current_account.id, domain] - end + def remove_domains_from_followers! + RemoveDomainsFromFollowersService.new.call(current_account, account_domains) end def account_domains diff --git a/app/models/status.rb b/app/models/status.rb index bf102120eb..d053dea44e 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -237,6 +237,16 @@ class Status < ApplicationRecord public_visibility? || unlisted_visibility? end + def translatable? + translate_target_locale = I18n.locale.to_s.split(/[_-]/).first + + distributable? && + content.present? && + language != translate_target_locale && + TranslationService.configured? && + TranslationService.configured.supported?(language, translate_target_locale) + end + alias sign? distributable? def with_media? diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 2cac42e8de..45ee06e12c 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -44,7 +44,6 @@ class InitialStateSerializer < ActiveModel::Serializer timeline_preview: Setting.timeline_preview, activity_api_enabled: Setting.activity_api_enabled, single_user_mode: Rails.configuration.x.single_user_mode, - translation_enabled: TranslationService.configured?, trends_as_landing_page: Setting.trends_as_landing_page, status_page_url: Setting.status_page_url, } diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 659c45b835..ce08b6db8d 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer include FormattingHelper attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, - :sensitive, :spoiler_text, :visibility, :language, + :sensitive, :spoiler_text, :visibility, :language, :translatable, :uri, :url, :replies_count, :reblogs_count, :favourites_count, :edited_at @@ -52,6 +52,10 @@ class REST::StatusSerializer < ActiveModel::Serializer object.account.user_shows_application? || (current_user? && current_user.account_id == object.account_id) end + def translatable + current_user? && object.translatable? + end + def visibility # This visibility is masked behind "private" # to avoid API changes because there are no diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index e2c3700572..a48386ba2b 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -48,9 +48,9 @@ class BatchedRemoveStatusService < BaseService # Cannot be batched @status_id_cutoff = Mastodon::Snowflake.id_at(2.weeks.ago) - redis.pipelined do + redis.pipelined do |pipeline| statuses.each do |status| - unpush_from_public_timelines(status) + unpush_from_public_timelines(status, pipeline) end end end @@ -73,22 +73,22 @@ class BatchedRemoveStatusService < BaseService end end - def unpush_from_public_timelines(status) + def unpush_from_public_timelines(status, pipeline) return unless status.public_visibility? && status.id > @status_id_cutoff payload = Oj.dump(event: :delete, payload: status.id.to_s) - redis.publish('timeline:public', payload) - redis.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload) + pipeline.publish('timeline:public', payload) + pipeline.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload) if status.media_attachments.any? - redis.publish('timeline:public:media', payload) - redis.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload) + pipeline.publish('timeline:public:media', payload) + pipeline.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload) end status.tags.map { |tag| tag.name.mb_chars.downcase }.each do |hashtag| - redis.publish("timeline:hashtag:#{hashtag}", payload) - redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local? + pipeline.publish("timeline:hashtag:#{hashtag}", payload) + pipeline.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local? end end diff --git a/app/services/follow_migration_service.rb b/app/services/follow_migration_service.rb new file mode 100644 index 0000000000..cfe9093cbe --- /dev/null +++ b/app/services/follow_migration_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class FollowMigrationService < FollowService + # Follow an account with the same settings as another account, and unfollow the old account once the request is sent + # @param [Account] source_account From which to follow + # @param [Account] target_account Account to follow + # @param [Account] old_target_account Account to unfollow once the follow request has been sent to the new one + # @option [Boolean] bypass_locked Whether to immediately follow the new account even if it is locked + 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 + + super(source_account, target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true) + end + + private + + 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])) + + if @target_account.local? + LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request') + UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true) + elsif @target_account.activitypub? + ActivityPub::MigratedFollowDeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, @old_target_account.id) + end + + follow_request + end + + def direct_follow! + follow = super + UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true) + follow + end +end diff --git a/app/services/remove_domains_from_followers_service.rb b/app/services/remove_domains_from_followers_service.rb new file mode 100644 index 0000000000..d76763409d --- /dev/null +++ b/app/services/remove_domains_from_followers_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class RemoveDomainsFromFollowersService < BaseService + include Payloadable + + def call(source_account, target_domains) + source_account.passive_relationships.where(account_id: Account.where(domain: target_domains)).find_each do |follow| + follow.destroy + + create_notification(follow) if source_account.local? && !follow.account.local? && follow.account.activitypub? + end + end + + private + + def create_notification(follow) + ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.target_account_id, follow.account.inbox_url) + end + + def build_json(follow) + Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) + end +end diff --git a/app/services/translate_status_service.rb b/app/services/translate_status_service.rb index 539a0d9db5..92d8b62a05 100644 --- a/app/services/translate_status_service.rb +++ b/app/services/translate_status_service.rb @@ -6,7 +6,7 @@ class TranslateStatusService < BaseService include FormattingHelper def call(status, target_language) - raise Mastodon::NotPermittedError unless status.public_visibility? || status.unlisted_visibility? + raise Mastodon::NotPermittedError unless status.translatable? @status = status @content = status_content_format(@status) diff --git a/app/validators/ed25519_key_validator.rb b/app/validators/ed25519_key_validator.rb index 00a448d5a2..adf49296b2 100644 --- a/app/validators/ed25519_key_validator.rb +++ b/app/validators/ed25519_key_validator.rb @@ -6,7 +6,7 @@ class Ed25519KeyValidator < ActiveModel::EachValidator key = Base64.decode64(value) - record.errors[attribute] << I18n.t('crypto.errors.invalid_key') unless verified?(key) + record.errors.add(attribute, I18n.t('crypto.errors.invalid_key')) unless verified?(key) end private diff --git a/app/validators/ed25519_signature_validator.rb b/app/validators/ed25519_signature_validator.rb index 77a21b8373..0e74c231ec 100644 --- a/app/validators/ed25519_signature_validator.rb +++ b/app/validators/ed25519_signature_validator.rb @@ -8,7 +8,7 @@ class Ed25519SignatureValidator < ActiveModel::EachValidator signature = Base64.decode64(value) message = option_to_value(record, :message) - record.errors[attribute] << I18n.t('crypto.errors.invalid_signature') unless verified?(verify_key, signature, message) + record.errors.add(attribute, I18n.t('crypto.errors.invalid_signature')) unless verified?(verify_key, signature, message) end private diff --git a/app/views/accounts/show.rss.ruby b/app/views/accounts/show.rss.ruby index 34e29d483f..7a77511ceb 100644 --- a/app/views/accounts/show.rss.ruby +++ b/app/views/accounts/show.rss.ruby @@ -5,7 +5,7 @@ RSS::Builder.build do |doc| doc.image(full_asset_url(@account.avatar.url(:original)), display_name(@account), params[:tag].present? ? short_account_tag_url(@account, params[:tag]) : short_account_url(@account)) doc.last_build_date(@statuses.first.created_at) if @statuses.any? doc.icon(full_asset_url(@account.avatar.url(:original))) - doc.generator("Mastodon v#{Mastodon::Version.to_s}") + doc.generator("Mastodon v#{Mastodon::Version}") @statuses.each do |status| doc.item do |item| @@ -18,12 +18,12 @@ RSS::Builder.build do |doc| item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) end - status.ordered_media_attachments.each do |media| - item.media_content(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) do |media_content| - media_content.medium(media.gifv? ? 'image' : media.type.to_s) + status.ordered_media_attachments.each do |media_attachment| + item.media_content(full_asset_url(media_attachment.file.url(:original, false)), media_attachment.file.content_type, media_attachment.file.size) do |media_content| + media_content.medium(media_attachment.gifv? ? 'image' : media_attachment.type.to_s) media_content.rating(status.sensitive? ? 'adult' : 'nonadult') - media_content.description(media.description) if media.description.present? - media_content.thumbnail(media.thumbnail.url(:original, false)) if media.thumbnail? + media_content.description(media_attachment.description) if media_attachment.description.present? + media_content.thumbnail(media_attachment.thumbnail.url(:original, false)) if media_attachment.thumbnail? end end diff --git a/app/views/admin/statuses/show.html.haml b/app/views/admin/statuses/show.html.haml index 1e1e63f37d..e070e5872b 100644 --- a/app/views/admin/statuses/show.html.haml +++ b/app/views/admin/statuses/show.html.haml @@ -31,7 +31,7 @@ %td - if @status.trend.allowed? %abbr{ title: t('admin.trends.tags.current_score', score: @status.trend.score) }= t('admin.trends.tags.trending_rank', rank: @status.trend.rank) - - elsif @status.trend.requires_review? + - elsif @status.requires_review? = t('admin.trends.pending_review') - else = t('admin.trends.not_allowed_to_trend') diff --git a/app/views/relationships/show.html.haml b/app/views/relationships/show.html.haml index e1ead69456..fcda6317ec 100644 --- a/app/views/relationships/show.html.haml +++ b/app/views/relationships/show.html.haml @@ -45,7 +45,7 @@ = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('relationships.confirm_remove_selected_followers') } unless following_relationship? - = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship? + = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :remove_domains_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship? .batch-table__body - if @accounts.empty? = nothing_here 'nothing-here--under-tabs' diff --git a/app/views/tags/show.rss.ruby b/app/views/tags/show.rss.ruby index 8e0c2327b5..bbda1ad4b5 100644 --- a/app/views/tags/show.rss.ruby +++ b/app/views/tags/show.rss.ruby @@ -3,7 +3,7 @@ RSS::Builder.build do |doc| doc.description(I18n.t('rss.descriptions.tag', hashtag: @tag.display_name)) doc.link(tag_url(@tag)) doc.last_build_date(@statuses.first.created_at) if @statuses.any? - doc.generator("Mastodon v#{Mastodon::Version.to_s}") + doc.generator("Mastodon v#{Mastodon::Version}") @statuses.each do |status| doc.item do |item| @@ -16,12 +16,12 @@ RSS::Builder.build do |doc| item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) end - status.ordered_media_attachments.each do |media| - item.media_content(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) do |media_content| - media_content.medium(media.gifv? ? 'image' : media.type.to_s) + status.ordered_media_attachments.each do |media_attachment| + item.media_content(full_asset_url(media_attachment.file.url(:original, false)), media_attachment.file.content_type, media_attachment.file.size) do |media_content| + media_content.medium(media_attachment.gifv? ? 'image' : media_attachment.type.to_s) media_content.rating(status.sensitive? ? 'adult' : 'nonadult') - media_content.description(media.description) if media.description.present? - media_content.thumbnail(media.thumbnail.url(:original, false)) if media.thumbnail? + media_content.description(media_attachment.description) if media_attachment.description.present? + media_content.thumbnail(media_attachment.thumbnail.url(:original, false)) if media_attachment.thumbnail? end end diff --git a/app/views/user_mailer/appeal_rejected.html.haml b/app/views/user_mailer/appeal_rejected.html.haml index 75cd9d023b..c316a73fb5 100644 --- a/app/views/user_mailer/appeal_rejected.html.haml +++ b/app/views/user_mailer/appeal_rejected.html.haml @@ -17,7 +17,7 @@ %tbody %tr %td - = image_tag full_pack_url('media/images/mailer/icon_warning.png'), alt: '' + = image_tag full_pack_url('media/images/mailer/icon_flag.png'), alt: '' %h1= t 'user_mailer.appeal_rejected.title' diff --git a/app/views/well_known/host_meta/show.xml.ruby b/app/views/well_known/host_meta/show.xml.ruby index b4e867c5f8..bb5a01a1b5 100644 --- a/app/views/well_known/host_meta/show.xml.ruby +++ b/app/views/well_known/host_meta/show.xml.ruby @@ -9,4 +9,4 @@ doc << Ox::Element.new('XRD').tap do |xrd| end end -('' + Ox.dump(doc, effort: :tolerant)).force_encoding('UTF-8') +"#{Ox.dump(doc, effort: :tolerant)}".force_encoding('UTF-8') diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index d9153132b3..7c1c14766b 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -10,6 +10,16 @@ class ActivityPub::DeliveryWorker sidekiq_options queue: 'push', retry: 16, dead: false + # Unfortunately, we cannot control Sidekiq's jitter, so add our own + sidekiq_retry_in do |count| + # This is Sidekiq's default delay + delay = (count**4) + 15 + # Our custom jitter, that will be added to Sidekiq's built-in one. + # Sidekiq's built-in jitter is `rand(10) * (count + 1)` + jitter = rand(0.5 * (count**4)) + delay + jitter + end + HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze def perform(json, source_account_id, inbox_url, options = {}) diff --git a/app/workers/activitypub/migrated_follow_delivery_worker.rb b/app/workers/activitypub/migrated_follow_delivery_worker.rb new file mode 100644 index 0000000000..daf30e0ae7 --- /dev/null +++ b/app/workers/activitypub/migrated_follow_delivery_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ActivityPub::MigratedFollowDeliveryWorker < ActivityPub::DeliveryWorker + def perform(json, source_account_id, inbox_url, old_target_account_id, options = {}) + super(json, source_account_id, inbox_url, options) + unfollow_old_account!(old_target_account_id) + end + + private + + def unfollow_old_account!(old_target_account_id) + old_target_account = Account.find(old_target_account_id) + UnfollowService.new.call(@source_account, old_target_account, skip_unmerge: true) + rescue + true + end +end diff --git a/app/workers/scheduler/follow_recommendations_scheduler.rb b/app/workers/scheduler/follow_recommendations_scheduler.rb index 04008a9d99..17cf3f2cc3 100644 --- a/app/workers/scheduler/follow_recommendations_scheduler.rb +++ b/app/workers/scheduler/follow_recommendations_scheduler.rb @@ -20,7 +20,7 @@ class Scheduler::FollowRecommendationsScheduler Trends.available_locales.each do |locale| recommendations = if AccountSummary.safe.filtered.localized(locale).exists? # We can skip the work if no accounts with that language exist - FollowRecommendation.localized(locale).order(rank: :desc).limit(SET_SIZE).map { |recommendation| [recommendation.account_id, recommendation.rank] } + FollowRecommendation.localized(locale).order(rank: :desc).limit(SET_SIZE).map { |recommendation| [recommendation.rank, recommendation.account_id] } else [] end @@ -33,14 +33,14 @@ class Scheduler::FollowRecommendationsScheduler # Language-specific results should be above language-agnostic ones, # otherwise language-agnostic ones will always overshadow them - recommendations.map! { |(account_id, rank)| [account_id, rank + max_fallback_rank] } + recommendations.map! { |(rank, account_id)| [rank + max_fallback_rank, account_id] } added = 0 fallback_recommendations.each do |recommendation| - next if recommendations.any? { |(account_id, _)| account_id == recommendation.account_id } + next if recommendations.any? { |(_, account_id)| account_id == recommendation.account_id } - recommendations << [recommendation.account_id, recommendation.rank] + recommendations << [recommendation.rank, recommendation.account_id] added += 1 break if added >= missing @@ -49,10 +49,7 @@ class Scheduler::FollowRecommendationsScheduler redis.multi do |multi| multi.del(key(locale)) - - recommendations.each do |(account_id, rank)| - multi.zadd(key(locale), rank, account_id) - end + multi.zadd(key(locale), recommendations) end end end diff --git a/app/workers/unfollow_follow_worker.rb b/app/workers/unfollow_follow_worker.rb index 7203b4888f..a4d57839de 100644 --- a/app/workers/unfollow_follow_worker.rb +++ b/app/workers/unfollow_follow_worker.rb @@ -10,13 +10,7 @@ class UnfollowFollowWorker old_target_account = Account.find(old_target_account_id) new_target_account = Account.find(new_target_account_id) - follow = follower_account.active_relationships.find_by(target_account: old_target_account) - reblogs = follow&.show_reblogs? - notify = follow&.notify? - languages = follow&.languages - - FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true) - UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true) + FollowMigrationService.new.call(follower_account, new_target_account, old_target_account, bypass_locked: bypass_locked) rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError true end diff --git a/config/database.yml b/config/database.yml index bfb53f21b4..34acf2f19a 100644 --- a/config/database.yml +++ b/config/database.yml @@ -5,6 +5,7 @@ default: &default connect_timeout: 15 encoding: unicode sslmode: <%= ENV['DB_SSLMODE'] || "prefer" %> + application_name: '' development: <<: *default diff --git a/config/environments/development.rb b/config/environments/development.rb index de8762ff74..29b17a3500 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -87,6 +87,8 @@ Rails.application.configure do config.x.otp_secret = ENV.fetch('OTP_SECRET', '1fc2b87989afa6351912abeebe31ffc5c476ead9bf8b3d74cbc4a302c7b69a45b40b1bbef3506ddad73e942e15ed5ca4b402bf9a66423626051104f4b5f05109') end +Redis.raise_deprecations = true + ActiveRecordQueryTrace.enabled = ENV['QUERY_TRACE_ENABLED'] == 'true' module PrivateAddressCheck diff --git a/config/environments/production.rb b/config/environments/production.rb index bc3bff6095..a241621ed3 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -128,6 +128,7 @@ Rails.application.configure do enable_starttls_auto: enable_starttls_auto, tls: ENV['SMTP_TLS'].presence && ENV['SMTP_TLS'] == 'true', ssl: ENV['SMTP_SSL'].presence && ENV['SMTP_SSL'] == 'true', + read_timeout: 20, } config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym diff --git a/config/environments/test.rb b/config/environments/test.rb index ef3cb2e487..9cbf31e8d7 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -73,3 +73,5 @@ end # Catch serialization warnings early Sidekiq.strict_args! + +Redis.raise_deprecations = true diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index a2285427c8..9282c941da 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -90,6 +90,12 @@ 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 + # Some S3-compatible providers might not actually be compatible with some APIs # used by kt-paperclip, see https://github.com/mastodon/mastodon/issues/16822 if ENV['S3_FORCE_SINGLE_REQUEST'] == 'true' diff --git a/config/initializers/redis.rb b/config/initializers/redis.rb new file mode 100644 index 0000000000..f2bbd1e456 --- /dev/null +++ b/config/initializers/redis.rb @@ -0,0 +1 @@ +Redis.sadd_returns_boolean = false diff --git a/config/locales-glitch/es.yml b/config/locales-glitch/es.yml index 1cc1aca610..05a9313723 100644 --- a/config/locales-glitch/es.yml +++ b/config/locales-glitch/es.yml @@ -2,8 +2,8 @@ es: admin: custom_emojis: - batch_copy_error: 'Se produjo un error cuando se copian algunos emojis seleccionados %{message}' - batch_error: 'Ocurrió un error %{message}' + batch_copy_error: Se produjo un error cuando se copian algunos emojis seleccionados %{message} + batch_error: Ocurrió un error %{message} settings: captcha_enabled: title: Requerir que usuarixs nuevxs resuelvan un CAPTCHA para confirmar su cuenta diff --git a/config/locales/en.yml b/config/locales/en.yml index 9f8ba7ce78..97d0999e4a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1408,6 +1408,7 @@ en: confirm_remove_selected_followers: Are you sure you want to remove selected followers? confirm_remove_selected_follows: Are you sure you want to remove selected follows? dormant: Dormant + follow_failure: Could not follow some of the selected accounts. follow_selected_followers: Follow selected followers followers: Followers following: Following @@ -1447,6 +1448,7 @@ en: electron: Electron firefox: Firefox generic: Unknown browser + huawei_browser: Huawei Browser ie: Internet Explorer micro_messenger: MicroMessenger nokia: Nokia S40 Ovi Browser @@ -1456,6 +1458,7 @@ en: qq: QQ Browser safari: Safari uc_browser: UC Browser + unknown_browser: Unknown Browser weibo: Weibo current_session: Current session description: "%{browser} on %{platform}" @@ -1468,9 +1471,10 @@ en: chrome_os: ChromeOS firefox_os: Firefox OS ios: iOS + kai_os: KaiOS linux: Linux mac: macOS - other: unknown platform + unknown_platform: Unknown Platform windows: Windows windows_mobile: Windows Mobile windows_phone: Windows Phone diff --git a/db/migrate/20170920032311_fix_reblogs_in_feeds.rb b/db/migrate/20170920032311_fix_reblogs_in_feeds.rb index 4ab68e8f32..7e2db0ff3e 100644 --- a/db/migrate/20170920032311_fix_reblogs_in_feeds.rb +++ b/db/migrate/20170920032311_fix_reblogs_in_feeds.rb @@ -1,6 +1,6 @@ class FixReblogsInFeeds < ActiveRecord::Migration[5.1] def up - redis = Redis.current + redis = RedisConfiguration.pool.checkout fm = FeedManager.instance # Old scheme: diff --git a/db/migrate/20200407202420_migrate_unavailable_inboxes.rb b/db/migrate/20200407202420_migrate_unavailable_inboxes.rb index 92a3acb5d1..8f9c687942 100644 --- a/db/migrate/20200407202420_migrate_unavailable_inboxes.rb +++ b/db/migrate/20200407202420_migrate_unavailable_inboxes.rb @@ -2,7 +2,8 @@ class MigrateUnavailableInboxes < ActiveRecord::Migration[5.2] disable_ddl_transaction! def up - urls = Redis.current.smembers('unavailable_inboxes') + redis = RedisConfiguration.pool.checkout + urls = redis.smembers('unavailable_inboxes') hosts = urls.map do |url| Addressable::URI.parse(url).normalized_host @@ -14,7 +15,7 @@ class MigrateUnavailableInboxes < ActiveRecord::Migration[5.2] UnavailableDomain.create(domain: host) end - Redis.current.del(*(['unavailable_inboxes'] + Redis.current.keys('exhausted_deliveries:*'))) + redis.del(*(['unavailable_inboxes'] + redis.keys('exhausted_deliveries:*'))) end def down; end diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index db379eb853..98855cbd06 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -627,7 +627,7 @@ module Mastodon exit(1) end - unless options[:force] || migration.target_acount_id == account.moved_to_account_id + unless options[:force] || migration.target_account_id == account.moved_to_account_id say('The specified account is not redirecting to its last migration target. Use --force if you want to replay the migration anyway', :red) exit(1) end diff --git a/lib/mastodon/feeds_cli.rb b/lib/mastodon/feeds_cli.rb index 428d63a446..fcfb487404 100644 --- a/lib/mastodon/feeds_cli.rb +++ b/lib/mastodon/feeds_cli.rb @@ -53,11 +53,7 @@ module Mastodon desc 'clear', 'Remove all home and list feeds from Redis' def clear keys = redis.keys('feed:*') - - redis.pipelined do - keys.each { |key| redis.del(key) } - end - + redis.del(keys) say('OK', :green) end end diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb index 3b0331e0bf..dfc5865615 100644 --- a/lib/sanitize_ext/sanitize_config.rb +++ b/lib/sanitize_ext/sanitize_config.rb @@ -73,12 +73,11 @@ class Sanitize elements: %w(p br span a abbr del pre blockquote code b strong u sub sup i em h1 h2 h3 h4 h5 ul ol li), attributes: { - 'a' => %w(href rel class title), - 'span' => %w(class), - 'abbr' => %w(title), + 'a' => %w(href rel class title), + 'span' => %w(class), 'blockquote' => %w(cite), - 'ol' => %w(start reversed), - 'li' => %w(value), + 'ol' => %w(start reversed), + 'li' => %w(value), }, add_attributes: { diff --git a/package.json b/package.json index e5e0c2946e..9dc316b2aa 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "object.values": "^1.1.6", "path-complete-extname": "^1.0.0", "pg": "^8.5.0", + "pg-connection-string": "^2.5.0", "postcss": "^8.4.21", "postcss-loader": "^3.0.0", "promise.prototype.finally": "^3.1.4", diff --git a/spec/controllers/admin/account_actions_controller_spec.rb b/spec/controllers/admin/account_actions_controller_spec.rb new file mode 100644 index 0000000000..4eae51c7b5 --- /dev/null +++ b/spec/controllers/admin/account_actions_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::AccountActionsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #new' do + let(:account) { Fabricate(:account) } + + it 'returns http success' do + get :new, params: { account_id: account.id } + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/announcements_controller_spec.rb b/spec/controllers/admin/announcements_controller_spec.rb new file mode 100644 index 0000000000..288ac1d713 --- /dev/null +++ b/spec/controllers/admin/announcements_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::AnnouncementsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/follow_recommendations_controller_spec.rb b/spec/controllers/admin/follow_recommendations_controller_spec.rb new file mode 100644 index 0000000000..f62aa6e4b2 --- /dev/null +++ b/spec/controllers/admin/follow_recommendations_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::FollowRecommendationsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/ip_blocks_controller_spec.rb b/spec/controllers/admin/ip_blocks_controller_spec.rb new file mode 100644 index 0000000000..873888afc7 --- /dev/null +++ b/spec/controllers/admin/ip_blocks_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::IpBlocksController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/relationships_controller_spec.rb b/spec/controllers/admin/relationships_controller_spec.rb new file mode 100644 index 0000000000..1099a37a3b --- /dev/null +++ b/spec/controllers/admin/relationships_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::RelationshipsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + let(:account) { Fabricate(:account) } + + it 'returns http success' do + get :index, params: { account_id: account.id } + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/relays_controller_spec.rb b/spec/controllers/admin/relays_controller_spec.rb new file mode 100644 index 0000000000..dfb9f3c048 --- /dev/null +++ b/spec/controllers/admin/relays_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::RelaysController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/reports/actions_controller_spec.rb b/spec/controllers/admin/reports/actions_controller_spec.rb index 3e42e4cb19..4c2624a408 100644 --- a/spec/controllers/admin/reports/actions_controller_spec.rb +++ b/spec/controllers/admin/reports/actions_controller_spec.rb @@ -57,6 +57,9 @@ describe Admin::Reports::ActionsController do let!(:media) { Fabricate(:media_attachment, account: target_account, status: statuses[0]) } let(:report) { Fabricate(:report, target_account: target_account, status_ids: statuses.map(&:id)) } let(:text) { 'hello' } + let(:common_params) do + { report_id: report.id, text: text } + end shared_examples 'common behavior' do it 'closes the report' do @@ -72,6 +75,26 @@ describe Admin::Reports::ActionsController do subject expect(response).to redirect_to(admin_reports_path) end + + context 'when text is unset' do + let(:common_params) do + { report_id: report.id } + end + + it 'closes the report' do + expect { subject }.to change { report.reload.action_taken? }.from(false).to(true) + end + + it 'creates a strike with the expected text' do + expect { subject }.to change { report.target_account.strikes.count }.by(1) + expect(report.target_account.strikes.last.text).to eq '' + end + + it 'redirects' do + subject + expect(response).to redirect_to(admin_reports_path) + end + end end shared_examples 'all action types' do @@ -124,13 +147,13 @@ describe Admin::Reports::ActionsController do end context 'action as submit button' do - subject { post :create, params: { report_id: report.id, text: text, action => '' } } + subject { post :create, params: common_params.merge({ action => '' }) } it_behaves_like 'all action types' end context 'action as submit button' do - subject { post :create, params: { report_id: report.id, text: text, moderation_action: action } } + subject { post :create, params: common_params.merge({ moderation_action: action }) } it_behaves_like 'all action types' end diff --git a/spec/controllers/admin/rules_controller_spec.rb b/spec/controllers/admin/rules_controller_spec.rb new file mode 100644 index 0000000000..d7b633c049 --- /dev/null +++ b/spec/controllers/admin/rules_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::RulesController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/settings/about_controller_spec.rb b/spec/controllers/admin/settings/about_controller_spec.rb new file mode 100644 index 0000000000..2ae26090b6 --- /dev/null +++ b/spec/controllers/admin/settings/about_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Settings::AboutController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/settings/appearance_controller_spec.rb b/spec/controllers/admin/settings/appearance_controller_spec.rb new file mode 100644 index 0000000000..65b29acc3e --- /dev/null +++ b/spec/controllers/admin/settings/appearance_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Settings::AppearanceController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/settings/content_retention_controller_spec.rb b/spec/controllers/admin/settings/content_retention_controller_spec.rb new file mode 100644 index 0000000000..53ce84d189 --- /dev/null +++ b/spec/controllers/admin/settings/content_retention_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Settings::ContentRetentionController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/settings/discovery_controller_spec.rb b/spec/controllers/admin/settings/discovery_controller_spec.rb new file mode 100644 index 0000000000..c7307ffc88 --- /dev/null +++ b/spec/controllers/admin/settings/discovery_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Settings::DiscoveryController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/settings/registrations_controller_spec.rb b/spec/controllers/admin/settings/registrations_controller_spec.rb new file mode 100644 index 0000000000..3fc1f9d132 --- /dev/null +++ b/spec/controllers/admin/settings/registrations_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Settings::RegistrationsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/site_uploads_controller_spec.rb b/spec/controllers/admin/site_uploads_controller_spec.rb new file mode 100644 index 0000000000..4ea37f396a --- /dev/null +++ b/spec/controllers/admin/site_uploads_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::SiteUploadsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'DELETE #destroy' do + let(:site_upload) { Fabricate(:site_upload, var: 'thumbnail') } + + it 'returns http success' do + delete :destroy, params: { id: site_upload.id } + + expect(response).to redirect_to(admin_settings_path) + end + end +end diff --git a/spec/controllers/admin/trends/links/preview_card_providers_controller_spec.rb b/spec/controllers/admin/trends/links/preview_card_providers_controller_spec.rb new file mode 100644 index 0000000000..95ed38d6b1 --- /dev/null +++ b/spec/controllers/admin/trends/links/preview_card_providers_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Trends::Links::PreviewCardProvidersController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/trends/links_controller_spec.rb b/spec/controllers/admin/trends/links_controller_spec.rb new file mode 100644 index 0000000000..7c67f5e5aa --- /dev/null +++ b/spec/controllers/admin/trends/links_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Trends::LinksController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/trends/statuses_controller_spec.rb b/spec/controllers/admin/trends/statuses_controller_spec.rb new file mode 100644 index 0000000000..b752234d3c --- /dev/null +++ b/spec/controllers/admin/trends/statuses_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Trends::StatusesController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/trends/tags_controller_spec.rb b/spec/controllers/admin/trends/tags_controller_spec.rb new file mode 100644 index 0000000000..4f74a55455 --- /dev/null +++ b/spec/controllers/admin/trends/tags_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Trends::TagsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/warning_presets_controller_spec.rb b/spec/controllers/admin/warning_presets_controller_spec.rb new file mode 100644 index 0000000000..6b48fc28bb --- /dev/null +++ b/spec/controllers/admin/warning_presets_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::WarningPresetsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/webhooks/secrets_controller_spec.rb b/spec/controllers/admin/webhooks/secrets_controller_spec.rb new file mode 100644 index 0000000000..291a10fba5 --- /dev/null +++ b/spec/controllers/admin/webhooks/secrets_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Webhooks::SecretsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'POST #rotate' do + let(:webhook) { Fabricate(:webhook) } + + it 'returns http success' do + post :rotate, params: { webhook_id: webhook.id } + + expect(response).to redirect_to(admin_webhook_path(webhook)) + end + end +end diff --git a/spec/controllers/admin/webhooks_controller_spec.rb b/spec/controllers/admin/webhooks_controller_spec.rb new file mode 100644 index 0000000000..12727e142b --- /dev/null +++ b/spec/controllers/admin/webhooks_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::WebhooksController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb b/spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb new file mode 100644 index 0000000000..bb075261f3 --- /dev/null +++ b/spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Accounts::FamiliarFollowersController do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb b/spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb new file mode 100644 index 0000000000..53ac1e2a7a --- /dev/null +++ b/spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Accounts::FeaturedTagsController do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/accounts/identity_proofs_controller_spec.rb b/spec/controllers/api/v1/accounts/identity_proofs_controller_spec.rb new file mode 100644 index 0000000000..6351de7616 --- /dev/null +++ b/spec/controllers/api/v1/accounts/identity_proofs_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Accounts::IdentityProofsController do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/accounts/lookup_controller_spec.rb b/spec/controllers/api/v1/accounts/lookup_controller_spec.rb new file mode 100644 index 0000000000..37407766f2 --- /dev/null +++ b/spec/controllers/api/v1/accounts/lookup_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Accounts::LookupController do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #show' do + it 'returns http success' do + get :show, params: { account_id: account.id, acct: account.acct } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb new file mode 100644 index 0000000000..3acae843ad --- /dev/null +++ b/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Admin::CanonicalEmailBlocksController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/admin/dimensions_controller_spec.rb b/spec/controllers/api/v1/admin/dimensions_controller_spec.rb new file mode 100644 index 0000000000..ea18efe383 --- /dev/null +++ b/spec/controllers/api/v1/admin/dimensions_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Admin::DimensionsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'POST #create' do + it 'returns http success' do + post :create, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb new file mode 100644 index 0000000000..a92a298699 --- /dev/null +++ b/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Admin::EmailDomainBlocksController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb new file mode 100644 index 0000000000..50e2ae9687 --- /dev/null +++ b/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Admin::IpBlocksController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/admin/measures_controller_spec.rb b/spec/controllers/api/v1/admin/measures_controller_spec.rb new file mode 100644 index 0000000000..03727a6329 --- /dev/null +++ b/spec/controllers/api/v1/admin/measures_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Admin::MeasuresController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'POST #create' do + it 'returns http success' do + post :create, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/admin/retention_controller_spec.rb b/spec/controllers/api/v1/admin/retention_controller_spec.rb new file mode 100644 index 0000000000..2381dbcb48 --- /dev/null +++ b/spec/controllers/api/v1/admin/retention_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Admin::RetentionController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'POST #create' do + it 'returns http success' do + post :create, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/admin/trends/links_controller_spec.rb b/spec/controllers/api/v1/admin/trends/links_controller_spec.rb new file mode 100644 index 0000000000..a64292f067 --- /dev/null +++ b/spec/controllers/api/v1/admin/trends/links_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Admin::Trends::LinksController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb b/spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb new file mode 100644 index 0000000000..821cc499f4 --- /dev/null +++ b/spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Admin::Trends::StatusesController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/admin/trends/tags_controller_spec.rb b/spec/controllers/api/v1/admin/trends/tags_controller_spec.rb new file mode 100644 index 0000000000..480306ce7e --- /dev/null +++ b/spec/controllers/api/v1/admin/trends/tags_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Admin::Trends::TagsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/directories_controller_spec.rb b/spec/controllers/api/v1/directories_controller_spec.rb new file mode 100644 index 0000000000..b18aedc4d1 --- /dev/null +++ b/spec/controllers/api/v1/directories_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::DirectoriesController do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb b/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb new file mode 100644 index 0000000000..54c63dcc6f --- /dev/null +++ b/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::FeaturedTags::SuggestionsController do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/featured_tags_controller_spec.rb b/spec/controllers/api/v1/featured_tags_controller_spec.rb new file mode 100644 index 0000000000..aac9429015 --- /dev/null +++ b/spec/controllers/api/v1/featured_tags_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::FeaturedTagsController do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index, params: { account_id: account.id, limit: 2 } + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/instances/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/instances/domain_blocks_controller_spec.rb new file mode 100644 index 0000000000..08f505c3d4 --- /dev/null +++ b/spec/controllers/api/v1/instances/domain_blocks_controller_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Instances::DomainBlocksController do + render_views + + describe 'GET #index' do + it 'returns http success' do + Setting.show_domain_blocks = 'all' + get :index + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/instances/extended_descriptions_controller_spec.rb b/spec/controllers/api/v1/instances/extended_descriptions_controller_spec.rb new file mode 100644 index 0000000000..58c0d4b8f1 --- /dev/null +++ b/spec/controllers/api/v1/instances/extended_descriptions_controller_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Instances::ExtendedDescriptionsController do + render_views + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/instances/privacy_policies_controller_spec.rb b/spec/controllers/api/v1/instances/privacy_policies_controller_spec.rb new file mode 100644 index 0000000000..ac0bed9dc6 --- /dev/null +++ b/spec/controllers/api/v1/instances/privacy_policies_controller_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Instances::PrivacyPoliciesController do + render_views + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/instances/rules_controller_spec.rb b/spec/controllers/api/v1/instances/rules_controller_spec.rb new file mode 100644 index 0000000000..5af50239b0 --- /dev/null +++ b/spec/controllers/api/v1/instances/rules_controller_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Instances::RulesController do + render_views + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/preferences_controller_spec.rb b/spec/controllers/api/v1/preferences_controller_spec.rb new file mode 100644 index 0000000000..79cc3066e1 --- /dev/null +++ b/spec/controllers/api/v1/preferences_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::PreferencesController do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/scheduled_statuses_controller_spec.rb b/spec/controllers/api/v1/scheduled_statuses_controller_spec.rb new file mode 100644 index 0000000000..256c4b272a --- /dev/null +++ b/spec/controllers/api/v1/scheduled_statuses_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::ScheduledStatusesController do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') } + let(:account) { Fabricate(:account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/statuses/translations_controller_spec.rb b/spec/controllers/api/v1/statuses/translations_controller_spec.rb new file mode 100644 index 0000000000..2deea9fc0c --- /dev/null +++ b/spec/controllers/api/v1/statuses/translations_controller_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Statuses::TranslationsController do + render_views + + let(:user) { Fabricate(:user) } + let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses', application: app) } + + context 'with an oauth token' do + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'POST #create' do + let(:status) { Fabricate(:status, account: user.account, text: 'Hola', language: 'es') } + + before do + translation = TranslationService::Translation.new(text: 'Hello') + service = instance_double(TranslationService::DeepL, translate: translation, supported?: true) + allow(TranslationService).to receive(:configured?).and_return(true) + allow(TranslationService).to receive(:configured).and_return(service) + post :create, params: { status_id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + end +end diff --git a/spec/controllers/api/v1/trends/links_controller_spec.rb b/spec/controllers/api/v1/trends/links_controller_spec.rb new file mode 100644 index 0000000000..71a7e2e477 --- /dev/null +++ b/spec/controllers/api/v1/trends/links_controller_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Trends::LinksController do + render_views + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/api/v1/trends/statuses_controller_spec.rb b/spec/controllers/api/v1/trends/statuses_controller_spec.rb new file mode 100644 index 0000000000..e9892bb140 --- /dev/null +++ b/spec/controllers/api/v1/trends/statuses_controller_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Trends::StatusesController do + render_views + + describe 'GET #index' do + it 'returns http success' do + get :index + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/relationships_controller_spec.rb b/spec/controllers/relationships_controller_spec.rb index 39f455e038..53a5daa517 100644 --- a/spec/controllers/relationships_controller_spec.rb +++ b/spec/controllers/relationships_controller_spec.rb @@ -58,7 +58,7 @@ describe RelationshipsController do end context 'when select parameter is provided' do - subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, block_domains: '' } } + subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, remove_domains_from_followers: '' } } it 'soft-blocks followers from selected domains' do poopfeast.follow!(user.account) @@ -69,6 +69,15 @@ describe RelationshipsController do expect(poopfeast.following?(user.account)).to be false end + it 'does not unfollow users from selected domains' do + user.account.follow!(poopfeast) + + sign_in user, scope: :user + subject + + expect(user.account.following?(poopfeast)).to be true + end + include_examples 'authenticate user' include_examples 'redirects back to followers page' end diff --git a/spec/controllers/settings/aliases_controller_spec.rb b/spec/controllers/settings/aliases_controller_spec.rb new file mode 100644 index 0000000000..805f659886 --- /dev/null +++ b/spec/controllers/settings/aliases_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Settings::AliasesController do + render_views + + let!(:user) { Fabricate(:user) } + let(:account) { user.account } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/settings/exports/blocked_domains_controller_spec.rb b/spec/controllers/settings/exports/blocked_domains_controller_spec.rb new file mode 100644 index 0000000000..ac72fd9dd7 --- /dev/null +++ b/spec/controllers/settings/exports/blocked_domains_controller_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Settings::Exports::BlockedDomainsController do + render_views + + describe 'GET #index' do + it 'returns a csv of the domains' do + account = Fabricate(:account, domain: 'example.com') + user = Fabricate(:user, account: account) + Fabricate(:account_domain_block, domain: 'example.com', account: account) + + sign_in user, scope: :user + get :index, format: :csv + + expect(response.body).to eq "example.com\n" + end + end +end diff --git a/spec/controllers/settings/exports/lists_controller_spec.rb b/spec/controllers/settings/exports/lists_controller_spec.rb new file mode 100644 index 0000000000..29623ba499 --- /dev/null +++ b/spec/controllers/settings/exports/lists_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Settings::Exports::ListsController do + render_views + + describe 'GET #index' do + it 'returns a csv of the domains' do + account = Fabricate(:account) + user = Fabricate(:user, account: account) + list = Fabricate(:list, account: account, title: 'The List') + Fabricate(:list_account, list: list, account: account) + + sign_in user, scope: :user + get :index, format: :csv + + expect(response.body).to match 'The List' + end + end +end diff --git a/spec/controllers/settings/login_activities_controller_spec.rb b/spec/controllers/settings/login_activities_controller_spec.rb new file mode 100644 index 0000000000..6f1f3de314 --- /dev/null +++ b/spec/controllers/settings/login_activities_controller_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Settings::LoginActivitiesController do + render_views + + let!(:user) { Fabricate(:user) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + it 'returns http success' do + get :index + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/settings/migration/redirects_controller_spec.rb b/spec/controllers/settings/migration/redirects_controller_spec.rb new file mode 100644 index 0000000000..50d9e1927b --- /dev/null +++ b/spec/controllers/settings/migration/redirects_controller_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Settings::Migration::RedirectsController do + render_views + + let!(:user) { Fabricate(:user) } + + before do + sign_in user, scope: :user + end + + describe 'GET #new' do + it 'returns http success' do + get :new + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/settings/pictures_controller_spec.rb b/spec/controllers/settings/pictures_controller_spec.rb new file mode 100644 index 0000000000..2368dc55dd --- /dev/null +++ b/spec/controllers/settings/pictures_controller_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Settings::PicturesController do + render_views + + let!(:user) { Fabricate(:user) } + + before do + sign_in user, scope: :user + end + + describe 'DELETE #destroy' do + context 'with invalid picture id' do + it 'returns http bad request' do + delete :destroy, params: { id: 'invalid' } + expect(response).to have_http_status(400) + end + end + end +end diff --git a/spec/controllers/settings/preferences/appearance_controller_spec.rb b/spec/controllers/settings/preferences/appearance_controller_spec.rb new file mode 100644 index 0000000000..7c7f716b71 --- /dev/null +++ b/spec/controllers/settings/preferences/appearance_controller_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Settings::Preferences::AppearanceController do + render_views + + let!(:user) { Fabricate(:user) } + + before do + sign_in user, scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/fabricators/account_alias_fabricator.rb b/spec/fabricators/account_alias_fabricator.rb deleted file mode 100644 index 4f434c078a..0000000000 --- a/spec/fabricators/account_alias_fabricator.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:account_alias) do - account - acct 'test@example.com' - uri 'https://example.com/users/test' -end diff --git a/spec/fabricators/account_deletion_request_fabricator.rb b/spec/fabricators/account_deletion_request_fabricator.rb deleted file mode 100644 index 3d3d373988..0000000000 --- a/spec/fabricators/account_deletion_request_fabricator.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:account_deletion_request) do - account -end diff --git a/spec/fabricators/account_migration_fabricator.rb b/spec/fabricators/account_migration_fabricator.rb index fd453f6d2a..ae6143a65c 100644 --- a/spec/fabricators/account_migration_fabricator.rb +++ b/spec/fabricators/account_migration_fabricator.rb @@ -5,4 +5,5 @@ Fabricator(:account_migration) do target_account { |attrs| Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(attrs[:account])]) } acct { |attrs| attrs[:target_account].acct } followers_count 1234 + created_at { 60.days.ago } end diff --git a/spec/fabricators/account_moderation_note_fabricator.rb b/spec/fabricators/account_moderation_note_fabricator.rb index 403870db6d..341a24dea0 100644 --- a/spec/fabricators/account_moderation_note_fabricator.rb +++ b/spec/fabricators/account_moderation_note_fabricator.rb @@ -2,5 +2,6 @@ Fabricator(:account_moderation_note) do content 'MyText' - account nil + account + target_account { Fabricate(:account) } end diff --git a/spec/fabricators/account_pin_fabricator.rb b/spec/fabricators/account_pin_fabricator.rb index 7d8a77bb54..32a5f3bdb8 100644 --- a/spec/fabricators/account_pin_fabricator.rb +++ b/spec/fabricators/account_pin_fabricator.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true Fabricator(:account_pin) do - account nil - target_account nil + account + target_account(fabricator: :account) + before_create { |account_pin, _| account_pin.account.follow!(account_pin.target_account) } end diff --git a/spec/fabricators/account_stat_fabricator.rb b/spec/fabricators/account_stat_fabricator.rb index 45b1524ef3..e6085c5f2b 100644 --- a/spec/fabricators/account_stat_fabricator.rb +++ b/spec/fabricators/account_stat_fabricator.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true Fabricator(:account_stat) do - account nil - statuses_count '' - following_count '' - followers_count '' + account + statuses_count '123' + following_count '456' + followers_count '789' end diff --git a/spec/fabricators/account_tag_stat_fabricator.rb b/spec/fabricators/account_tag_stat_fabricator.rb deleted file mode 100644 index 769015bd02..0000000000 --- a/spec/fabricators/account_tag_stat_fabricator.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:account_tag_stat) do - accounts_count '' -end diff --git a/spec/fabricators/account_warning_preset_fabricator.rb b/spec/fabricators/account_warning_preset_fabricator.rb index 7588e7f9cf..c50e08bf4f 100644 --- a/spec/fabricators/account_warning_preset_fabricator.rb +++ b/spec/fabricators/account_warning_preset_fabricator.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true Fabricator(:account_warning_preset) do - text 'MyText' + text { Faker::Lorem.paragraph } end diff --git a/spec/fabricators/admin_action_log_fabricator.rb b/spec/fabricators/admin_action_log_fabricator.rb index eb738c01c0..a259644bdc 100644 --- a/spec/fabricators/admin_action_log_fabricator.rb +++ b/spec/fabricators/admin_action_log_fabricator.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true Fabricator('Admin::ActionLog') do - account nil + account action 'MyString' target nil end diff --git a/spec/fabricators/announcement_mute_fabricator.rb b/spec/fabricators/announcement_mute_fabricator.rb deleted file mode 100644 index 109fec0412..0000000000 --- a/spec/fabricators/announcement_mute_fabricator.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:announcement_mute) do - account - announcement -end diff --git a/spec/fabricators/announcement_reaction_fabricator.rb b/spec/fabricators/announcement_reaction_fabricator.rb deleted file mode 100644 index 5da51caaa3..0000000000 --- a/spec/fabricators/announcement_reaction_fabricator.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:announcement_reaction) do - account - announcement - name '🌿' -end diff --git a/spec/fabricators/conversation_account_fabricator.rb b/spec/fabricators/conversation_account_fabricator.rb deleted file mode 100644 index f69d36855e..0000000000 --- a/spec/fabricators/conversation_account_fabricator.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:conversation_account) do - account nil - conversation nil - participant_account_ids '' - last_status nil -end diff --git a/spec/fabricators/conversation_mute_fabricator.rb b/spec/fabricators/conversation_mute_fabricator.rb deleted file mode 100644 index 5cf4dd3d59..0000000000 --- a/spec/fabricators/conversation_mute_fabricator.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:conversation_mute) do -end diff --git a/spec/fabricators/custom_emoji_category_fabricator.rb b/spec/fabricators/custom_emoji_category_fabricator.rb deleted file mode 100644 index 6019baba21..0000000000 --- a/spec/fabricators/custom_emoji_category_fabricator.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:custom_emoji_category) do - name 'MyString' -end diff --git a/spec/fabricators/encrypted_message_fabricator.rb b/spec/fabricators/encrypted_message_fabricator.rb deleted file mode 100644 index 2898827549..0000000000 --- a/spec/fabricators/encrypted_message_fabricator.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:encrypted_message) do - device - from_account - from_device_id { Faker::Number.number(digits: 5) } - type 0 - body '' - message_franking '' -end diff --git a/spec/fabricators/featured_tag_fabricator.rb b/spec/fabricators/featured_tag_fabricator.rb deleted file mode 100644 index 4bfa3e924e..0000000000 --- a/spec/fabricators/featured_tag_fabricator.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:featured_tag) do - account - tag - statuses_count 1_337 - last_status_at Time.now.utc -end diff --git a/spec/fabricators/follow_recommendation_suppression_fabricator.rb b/spec/fabricators/follow_recommendation_suppression_fabricator.rb deleted file mode 100644 index 6477baee11..0000000000 --- a/spec/fabricators/follow_recommendation_suppression_fabricator.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:follow_recommendation_suppression) do - account -end diff --git a/spec/fabricators/identity_fabricator.rb b/spec/fabricators/identity_fabricator.rb index b830101117..58072c0d65 100644 --- a/spec/fabricators/identity_fabricator.rb +++ b/spec/fabricators/identity_fabricator.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true Fabricator(:identity) do - user nil + user provider 'MyString' uid 'MyString' end diff --git a/spec/fabricators/import_fabricator.rb b/spec/fabricators/import_fabricator.rb deleted file mode 100644 index 11602f407f..0000000000 --- a/spec/fabricators/import_fabricator.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:import) do -end diff --git a/spec/fabricators/ip_block_fabricator.rb b/spec/fabricators/ip_block_fabricator.rb deleted file mode 100644 index a5da3f7065..0000000000 --- a/spec/fabricators/ip_block_fabricator.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:ip_block) do - ip '' - severity '' - expires_at '2020-10-08 22:20:37' - comment 'MyText' -end diff --git a/spec/fabricators/list_account_fabricator.rb b/spec/fabricators/list_account_fabricator.rb index b0af29e6fa..00dde83cdf 100644 --- a/spec/fabricators/list_account_fabricator.rb +++ b/spec/fabricators/list_account_fabricator.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true Fabricator(:list_account) do - list nil - account nil - follow nil + list + account + before_create { |list_account, _| list_account.list.account.follow!(account) } end diff --git a/spec/fabricators/one_time_key_fabricator.rb b/spec/fabricators/one_time_key_fabricator.rb index e317c28bd1..cfb365cabb 100644 --- a/spec/fabricators/one_time_key_fabricator.rb +++ b/spec/fabricators/one_time_key_fabricator.rb @@ -3,7 +3,7 @@ Fabricator(:one_time_key) do device key_id { Faker::Alphanumeric.alphanumeric(number: 10) } - key { Base64.strict_encode64(Ed25519::SigningKey.generate.verify_key.to_bytes) } + key { Base64.strict_encode64(Ed25519::SigningKey.generate.verify_key.to_bytes) } signature do |attrs| signing_key = Ed25519::SigningKey.generate diff --git a/spec/fabricators/preview_card_provider_fabricator.rb b/spec/fabricators/preview_card_provider_fabricator.rb new file mode 100644 index 0000000000..78db710003 --- /dev/null +++ b/spec/fabricators/preview_card_provider_fabricator.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Fabricator(:preview_card_provider) do + domain { Faker::Internet.domain_name } +end diff --git a/spec/fabricators/setting_fabricator.rb b/spec/fabricators/setting_fabricator.rb index 336d7c3551..ce9a48e901 100644 --- a/spec/fabricators/setting_fabricator.rb +++ b/spec/fabricators/setting_fabricator.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true Fabricator(:setting) do + var 'var' end diff --git a/spec/fabricators/site_upload_fabricator.rb b/spec/fabricators/site_upload_fabricator.rb index ad1b777cc4..87553ccb8a 100644 --- a/spec/fabricators/site_upload_fabricator.rb +++ b/spec/fabricators/site_upload_fabricator.rb @@ -2,4 +2,5 @@ Fabricator(:site_upload) do file { Rails.root.join('spec', 'fabricators', 'assets', 'utah_teapot.png').open } + var 'thumbnail' end diff --git a/spec/fabricators/status_edit_fabricator.rb b/spec/fabricators/status_edit_fabricator.rb deleted file mode 100644 index 33735a4592..0000000000 --- a/spec/fabricators/status_edit_fabricator.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:status_edit) do - status nil - account nil - text 'MyText' - spoiler_text 'MyText' - media_attachments_changed false -end diff --git a/spec/fabricators/status_stat_fabricator.rb b/spec/fabricators/status_stat_fabricator.rb deleted file mode 100644 index 8a358c51a9..0000000000 --- a/spec/fabricators/status_stat_fabricator.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:status_stat) do - status_id nil - replies_count '' - reblogs_count '' - favourites_count '' -end diff --git a/spec/fabricators/unavailable_domain_fabricator.rb b/spec/fabricators/unavailable_domain_fabricator.rb index 300a9e7a14..cb9707020a 100644 --- a/spec/fabricators/unavailable_domain_fabricator.rb +++ b/spec/fabricators/unavailable_domain_fabricator.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true Fabricator(:unavailable_domain) do - domain { Faker::Internet.domain } + domain { Faker::Internet.domain_name } end diff --git a/spec/fabricators/user_invite_request_fabricator.rb b/spec/fabricators/user_invite_request_fabricator.rb deleted file mode 100644 index 7736263e45..0000000000 --- a/spec/fabricators/user_invite_request_fabricator.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:user_invite_request) do - user - text { Faker::Lorem.sentence } -end diff --git a/spec/fabricators/web_setting_fabricator.rb b/spec/fabricators/web_setting_fabricator.rb deleted file mode 100644 index 7c9f300798..0000000000 --- a/spec/fabricators/web_setting_fabricator.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -Fabricator(:web_setting, from: Web::Setting) do -end diff --git a/spec/fabricators_spec.rb b/spec/fabricators_spec.rb new file mode 100644 index 0000000000..3b76c56ce6 --- /dev/null +++ b/spec/fabricators_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +Fabrication.manager.load_definitions if Fabrication.manager.empty? + +Fabrication.manager.schematics.map(&:first).each do |factory_name| + describe "The #{factory_name} factory" do + it 'is valid' do + factory = Fabricate(factory_name) + expect(factory).to be_valid + end + end +end diff --git a/spec/helpers/admin/account_moderation_notes_helper_spec.rb b/spec/helpers/admin/account_moderation_notes_helper_spec.rb index 622ce88065..e01eba51da 100644 --- a/spec/helpers/admin/account_moderation_notes_helper_spec.rb +++ b/spec/helpers/admin/account_moderation_notes_helper_spec.rb @@ -42,13 +42,11 @@ RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do let(:account) { Fabricate(:account) } it 'calls #link_to' do - expect(helper).to receive(:link_to).with( - admin_account_path(account.id), - class: name_tag_classes(account, true), - title: account.acct - ) + result = helper.admin_account_inline_link_to(account) - helper.admin_account_inline_link_to(account) + expect(result).to match(name_tag_classes(account, true)) + expect(result).to match(account.acct) + expect(result).to match(admin_account_path(account.id)) end end end diff --git a/spec/helpers/admin/dashboard_helper_spec.rb b/spec/helpers/admin/dashboard_helper_spec.rb new file mode 100644 index 0000000000..59062e4839 --- /dev/null +++ b/spec/helpers/admin/dashboard_helper_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::DashboardHelper do + describe 'relevant_account_timestamp' do + context 'with an account with older sign in' do + let(:account) { Fabricate(:account) } + let(:stamp) { 10.days.ago } + + it 'returns a time element' do + account.user.update(current_sign_in_at: stamp) + result = helper.relevant_account_timestamp(account) + + expect(result).to match('time-ago') + expect(result).to match(I18n.l(stamp)) + end + end + + context 'with an account with newer sign in' do + let(:account) { Fabricate(:account) } + + it 'returns a time element' do + account.user.update(current_sign_in_at: 10.hours.ago) + result = helper.relevant_account_timestamp(account) + + expect(result).to eq(I18n.t('generic.today')) + end + end + + context 'with an account where the user is pending' do + let(:account) { Fabricate(:account) } + + it 'returns a time element' do + account.user.update(current_sign_in_at: nil) + account.user.update(approved: false) + result = helper.relevant_account_timestamp(account) + + expect(result).to match('time-ago') + expect(result).to match(I18n.l(account.user.created_at)) + end + end + + context 'with an account with a last status value' do + let(:account) { Fabricate(:account) } + let(:stamp) { 5.minutes.ago } + + it 'returns a time element' do + account.user.update(current_sign_in_at: nil) + account.account_stat.update(last_status_at: stamp) + result = helper.relevant_account_timestamp(account) + + expect(result).to match('time-ago') + expect(result).to match(I18n.l(stamp)) + end + end + + context 'with an account without sign in or last status or pending' do + let(:account) { Fabricate(:account) } + + it 'returns a time element' do + account.user.update(current_sign_in_at: nil) + result = helper.relevant_account_timestamp(account) + + expect(result).to eq('-') + end + end + end +end diff --git a/spec/helpers/languages_helper_spec.rb b/spec/helpers/languages_helper_spec.rb index 217c9b2397..98c8064a33 100644 --- a/spec/helpers/languages_helper_spec.rb +++ b/spec/helpers/languages_helper_spec.rb @@ -10,14 +10,54 @@ describe LanguagesHelper do end describe 'native_locale_name' do - it 'finds the human readable native name from a key' do - expect(helper.native_locale_name(:de)).to eq('Deutsch') + context 'with a blank locale' do + it 'defaults to a generic value' do + expect(helper.native_locale_name(nil)).to eq(I18n.t('generic.none')) + end + end + + context 'with a locale of `und`' do + it 'defaults to a generic value' do + expect(helper.native_locale_name('und')).to eq(I18n.t('generic.none')) + end + end + + context 'with a supported locale' do + it 'finds the human readable native name from a key' do + expect(helper.native_locale_name(:de)).to eq('Deutsch') + end + end + + context 'with a regional locale' do + it 'finds the human readable regional name from a key' do + expect(helper.native_locale_name('en-GB')).to eq('English (British)') + end + end + + context 'with a non-existent locale' do + it 'returns the supplied locale value' do + expect(helper.native_locale_name(:xxx)).to eq(:xxx) + end end end describe 'standard_locale_name' do - it 'finds the human readable standard name from a key' do - expect(helper.standard_locale_name(:de)).to eq('German') + context 'with a blank locale' do + it 'defaults to a generic value' do + expect(helper.standard_locale_name(nil)).to eq(I18n.t('generic.none')) + end + end + + context 'with a non-existent locale' do + it 'returns the supplied locale value' do + expect(helper.standard_locale_name(:xxx)).to eq(:xxx) + end + end + + context 'with a supported locale' do + it 'finds the human readable standard name from a key' do + expect(helper.standard_locale_name(:de)).to eq('German') + end end end end diff --git a/spec/helpers/settings_helper_spec.rb b/spec/helpers/settings_helper_spec.rb new file mode 100644 index 0000000000..cba5c6ee89 --- /dev/null +++ b/spec/helpers/settings_helper_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe SettingsHelper do + describe 'session_device_icon' do + context 'with a mobile device' do + let(:session) { SessionActivation.new(user_agent: 'Mozilla/5.0 (iPhone)') } + + it 'detects the device and returns a descriptive string' do + result = helper.session_device_icon(session) + + expect(result).to eq('mobile') + end + end + + context 'with a tablet device' do + let(:session) { SessionActivation.new(user_agent: 'Mozilla/5.0 (iPad)') } + + it 'detects the device and returns a descriptive string' do + result = helper.session_device_icon(session) + + expect(result).to eq('tablet') + end + end + + context 'with a desktop device' do + let(:session) { SessionActivation.new(user_agent: 'Mozilla/5.0 (Macintosh)') } + + it 'detects the device and returns a descriptive string' do + result = helper.session_device_icon(session) + + expect(result).to eq('desktop') + end + end + end +end diff --git a/spec/helpers/statuses_helper_spec.rb b/spec/helpers/statuses_helper_spec.rb index ce2a4680e0..c8ca2ed323 100644 --- a/spec/helpers/statuses_helper_spec.rb +++ b/spec/helpers/statuses_helper_spec.rb @@ -3,6 +3,68 @@ require 'rails_helper' RSpec.describe StatusesHelper, type: :helper do + describe 'link_to_newer' do + it 'returns a link to newer content' do + url = 'https://example.com' + result = helper.link_to_newer(url) + + expect(result).to match('load-more') + expect(result).to match(I18n.t('statuses.show_newer')) + end + end + + describe 'link_to_older' do + it 'returns a link to older content' do + url = 'https://example.com' + result = helper.link_to_older(url) + + expect(result).to match('load-more') + expect(result).to match(I18n.t('statuses.show_older')) + end + end + + describe 'fa_visibility_icon' do + context 'with a status that is public' do + let(:status) { Status.new(visibility: 'public') } + + it 'returns the correct fa icon' do + result = helper.fa_visibility_icon(status) + + expect(result).to match('fa-globe') + end + end + + context 'with a status that is unlisted' do + let(:status) { Status.new(visibility: 'unlisted') } + + it 'returns the correct fa icon' do + result = helper.fa_visibility_icon(status) + + expect(result).to match('fa-unlock') + end + end + + context 'with a status that is private' do + let(:status) { Status.new(visibility: 'private') } + + it 'returns the correct fa icon' do + result = helper.fa_visibility_icon(status) + + expect(result).to match('fa-lock') + end + end + + context 'with a status that is direct' do + let(:status) { Status.new(visibility: 'direct') } + + it 'returns the correct fa icon' do + result = helper.fa_visibility_icon(status) + + expect(result).to match('fa-at') + end + end + end + describe '#stream_link_target' do it 'returns nil if it is not an embedded view' do set_not_embedded_view diff --git a/spec/lib/translation_service/deepl_spec.rb b/spec/lib/translation_service/deepl_spec.rb new file mode 100644 index 0000000000..aa24731860 --- /dev/null +++ b/spec/lib/translation_service/deepl_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe TranslationService::DeepL do + subject(:service) { described_class.new(plan, 'my-api-key') } + + let(:plan) { 'advanced' } + + before do + stub_request(:get, 'https://api.deepl.com/v2/languages?type=source').to_return( + body: '[{"language":"EN","name":"English"},{"language":"UK","name":"Ukrainian"}]' + ) + stub_request(:get, 'https://api.deepl.com/v2/languages?type=target').to_return( + body: '[{"language":"EN-GB","name":"English (British)"},{"language":"ZH","name":"Chinese"}]' + ) + end + + describe '#supported?' do + it 'supports included languages as source and target languages' do + expect(service.supported?('uk', 'en')).to be true + end + + it 'supports auto-detecting source language' do + expect(service.supported?(nil, 'en')).to be true + end + + it 'supports "en" and "pt" as target languages though not included in language list' do + expect(service.supported?('uk', 'en')).to be true + expect(service.supported?('uk', 'pt')).to be true + end + + it 'does not support non-included language as target language' do + expect(service.supported?('uk', 'nl')).to be false + end + + it 'does not support non-included language as source language' do + expect(service.supported?('da', 'en')).to be false + end + end + + describe '#translate' do + it 'returns translation with specified source language' do + stub_request(:post, 'https://api.deepl.com/v2/translate') + .with(body: 'text=Hasta+la+vista&source_lang=ES&target_lang=en&tag_handling=html') + .to_return(body: '{"translations":[{"detected_source_language":"ES","text":"See you soon"}]}') + + translation = service.translate('Hasta la vista', 'es', 'en') + expect(translation.detected_source_language).to eq 'es' + expect(translation.provider).to eq 'DeepL.com' + expect(translation.text).to eq 'See you soon' + end + + it 'returns translation with auto-detected source language' do + stub_request(:post, 'https://api.deepl.com/v2/translate') + .with(body: 'text=Guten+Tag&source_lang&target_lang=en&tag_handling=html') + .to_return(body: '{"translations":[{"detected_source_language":"DE","text":"Good Morning"}]}') + + translation = service.translate('Guten Tag', nil, 'en') + expect(translation.detected_source_language).to eq 'de' + expect(translation.provider).to eq 'DeepL.com' + expect(translation.text).to eq 'Good Morning' + end + end + + describe '#languages?' do + it 'returns source languages' do + expect(service.send(:languages, 'source')).to eq ['en', 'uk', nil] + end + + it 'returns target languages' do + expect(service.send(:languages, 'target')).to eq %w(en-gb zh en pt) + end + end + + describe '#request' do + before do + stub_request(:any, //) + # rubocop:disable Lint/EmptyBlock + service.send(:request, :get, '/v2/languages') { |res| } + # rubocop:enable Lint/EmptyBlock + end + + it 'uses paid plan base URL' do + expect(a_request(:get, 'https://api.deepl.com/v2/languages')).to have_been_made.once + end + + context 'with free plan' do + let(:plan) { 'free' } + + it 'uses free plan base URL' do + expect(a_request(:get, 'https://api-free.deepl.com/v2/languages')).to have_been_made.once + end + end + + it 'sends API key' do + expect(a_request(:get, 'https://api.deepl.com/v2/languages').with(headers: { Authorization: 'DeepL-Auth-Key my-api-key' })).to have_been_made.once + end + end +end diff --git a/spec/lib/translation_service/libre_translate_spec.rb b/spec/lib/translation_service/libre_translate_spec.rb new file mode 100644 index 0000000000..a6cb01884a --- /dev/null +++ b/spec/lib/translation_service/libre_translate_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe TranslationService::LibreTranslate do + subject(:service) { described_class.new('https://libretranslate.example.com', 'my-api-key') } + + before do + stub_request(:get, 'https://libretranslate.example.com/languages').to_return( + body: '[{"code": "en","name": "English","targets": ["de","es"]},{"code": "da","name": "Danish","targets": ["en","de"]}]' + ) + end + + describe '#supported?' do + it 'supports included language pair' do + expect(service.supported?('en', 'de')).to be true + end + + it 'does not support reversed language pair' do + expect(service.supported?('de', 'en')).to be false + end + + it 'supports auto-detecting source language' do + expect(service.supported?(nil, 'de')).to be true + end + + it 'does not support auto-detecting for unsupported target language' do + expect(service.supported?(nil, 'pt')).to be false + end + end + + describe '#languages' do + subject(:languages) { service.send(:languages) } + + it 'includes supported source languages' do + expect(languages.keys).to eq ['en', 'da', nil] + end + + it 'includes supported target languages for source language' do + expect(languages['en']).to eq %w(de es) + end + + it 'includes supported target languages for auto-detected language' do + expect(languages[nil]).to eq %w(de es en) + end + end + + describe '#translate' do + it 'returns translation with specified source language' do + stub_request(:post, 'https://libretranslate.example.com/translate') + .with(body: '{"q":"Hasta la vista","source":"es","target":"en","format":"html","api_key":"my-api-key"}') + .to_return(body: '{"translatedText": "See you"}') + + translation = service.translate('Hasta la vista', 'es', 'en') + expect(translation.detected_source_language).to eq 'es' + expect(translation.provider).to eq 'LibreTranslate' + expect(translation.text).to eq 'See you' + end + + it 'returns translation with auto-detected source language' do + stub_request(:post, 'https://libretranslate.example.com/translate') + .with(body: '{"q":"Guten Morgen","source":"auto","target":"en","format":"html","api_key":"my-api-key"}') + .to_return(body: '{"detectedLanguage":{"confidence":92,"language":"de"},"translatedText":"Good morning"}') + + translation = service.translate('Guten Morgen', nil, 'en') + expect(translation.detected_source_language).to be_nil + expect(translation.provider).to eq 'LibreTranslate' + expect(translation.text).to eq 'Good morning' + end + end +end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 9c22f60f1d..30824e7b4d 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -94,4 +94,52 @@ describe UserMailer, type: :mailer do expect(mail.body.encoded).to include strike.text end end + + describe 'webauthn_credential_deleted' do + let(:credential) { Fabricate(:webauthn_credential, user_id: receiver.id) } + let(:mail) { UserMailer.webauthn_credential_deleted(receiver, credential) } + + it 'renders webauthn credential deleted notification' do + receiver.update!(locale: nil) + expect(mail.body.encoded).to include I18n.t('devise.mailer.webauthn_credential.deleted.title') + end + + include_examples 'localized subject', + 'devise.mailer.webauthn_credential.deleted.subject' + end + + describe 'suspicious_sign_in' do + let(:ip) { '192.168.0.1' } + let(:agent) { 'NCSA_Mosaic/2.0 (Windows 3.1)' } + let(:timestamp) { Time.now.utc } + let(:mail) { UserMailer.suspicious_sign_in(receiver, ip, agent, timestamp) } + + it 'renders suspicious sign in notification' do + receiver.update!(locale: nil) + expect(mail.body.encoded).to include I18n.t('user_mailer.suspicious_sign_in.explanation') + end + + include_examples 'localized subject', + 'user_mailer.suspicious_sign_in.subject' + end + + describe 'appeal_approved' do + let(:appeal) { Fabricate(:appeal, account: receiver.account, approved_at: Time.now.utc) } + let(:mail) { UserMailer.appeal_approved(receiver, appeal) } + + it 'renders appeal_approved notification' do + expect(mail.subject).to eq I18n.t('user_mailer.appeal_approved.subject', date: I18n.l(appeal.created_at)) + expect(mail.body.encoded).to include I18n.t('user_mailer.appeal_approved.title') + end + end + + describe 'appeal_rejected' do + let(:appeal) { Fabricate(:appeal, account: receiver.account, rejected_at: Time.now.utc) } + let(:mail) { UserMailer.appeal_rejected(receiver, appeal) } + + it 'renders appeal_rejected notification' do + expect(mail.subject).to eq I18n.t('user_mailer.appeal_rejected.subject', date: I18n.l(appeal.created_at)) + expect(mail.body.encoded).to include I18n.t('user_mailer.appeal_rejected.title') + end + end end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 1e5a80963f..ae4e5ee321 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -704,12 +704,6 @@ RSpec.describe Account, type: :model do end describe 'validations' do - it 'has a valid fabricator' do - account = Fabricate.build(:account) - account.valid? - expect(account).to be_valid - end - it 'is invalid without a username' do account = Fabricate.build(:account, username: nil) account.valid? diff --git a/spec/models/account_warning_preset_spec.rb b/spec/models/account_warning_preset_spec.rb new file mode 100644 index 0000000000..f171df7c97 --- /dev/null +++ b/spec/models/account_warning_preset_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe AccountWarningPreset do + describe 'alphabetical' do + let(:first) { Fabricate(:account_warning_preset, title: 'aaa', text: 'aaa') } + let(:second) { Fabricate(:account_warning_preset, title: 'bbb', text: 'aaa') } + let(:third) { Fabricate(:account_warning_preset, title: 'bbb', text: 'bbb') } + + it 'returns records in order of title and text' do + results = described_class.alphabetic + + expect(results).to eq([first, second, third]) + end + end +end diff --git a/spec/models/appeal_spec.rb b/spec/models/appeal_spec.rb index 6aa013aba9..12373a9494 100644 --- a/spec/models/appeal_spec.rb +++ b/spec/models/appeal_spec.rb @@ -2,6 +2,37 @@ require 'rails_helper' -RSpec.describe Appeal, type: :model do - pending "add some examples to (or delete) #{__FILE__}" +describe Appeal do + describe 'scopes' do + describe 'approved' do + let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) } + let(:not_approved_appeal) { Fabricate(:appeal, approved_at: nil) } + + it 'finds the correct records' do + results = described_class.approved + expect(results).to eq([approved_appeal]) + end + end + + describe 'rejected' do + let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) } + let(:not_rejected_appeal) { Fabricate(:appeal, rejected_at: nil) } + + it 'finds the correct records' do + results = described_class.rejected + expect(results).to eq([rejected_appeal]) + end + end + + describe 'pending' do + let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) } + let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) } + let(:pending_appeal) { Fabricate(:appeal, rejected_at: nil, approved_at: nil) } + + it 'finds the correct records' do + results = described_class.pending + expect(results).to eq([pending_appeal]) + end + end + end end diff --git a/spec/models/block_spec.rb b/spec/models/block_spec.rb index 64c39fce60..6e31786d04 100644 --- a/spec/models/block_spec.rb +++ b/spec/models/block_spec.rb @@ -4,11 +4,6 @@ require 'rails_helper' RSpec.describe Block, type: :model do describe 'validations' do - it 'has a valid fabricator' do - block = Fabricate.build(:block) - expect(block).to be_valid - end - it 'is invalid without an account' do block = Fabricate.build(:block, account: nil) block.valid? diff --git a/spec/models/custom_emoji_category_spec.rb b/spec/models/custom_emoji_category_spec.rb index 74881b26c2..30de07bd81 100644 --- a/spec/models/custom_emoji_category_spec.rb +++ b/spec/models/custom_emoji_category_spec.rb @@ -2,6 +2,13 @@ require 'rails_helper' -RSpec.describe CustomEmojiCategory, type: :model do - pending "add some examples to (or delete) #{__FILE__}" +describe CustomEmojiCategory do + describe 'validations' do + it 'validates name presence' do + record = described_class.new(name: nil) + + expect(record).to_not be_valid + expect(record).to model_have_error_on_field(:name) + end + end end diff --git a/spec/models/domain_allow_spec.rb b/spec/models/domain_allow_spec.rb index 18cf5fe4c7..49e16376ea 100644 --- a/spec/models/domain_allow_spec.rb +++ b/spec/models/domain_allow_spec.rb @@ -2,6 +2,17 @@ require 'rails_helper' -RSpec.describe DomainAllow, type: :model do - pending "add some examples to (or delete) #{__FILE__}" +describe DomainAllow do + describe 'scopes' do + describe 'matches_domain' do + let(:domain) { Fabricate(:domain_allow, domain: 'example.com') } + let(:other_domain) { Fabricate(:domain_allow, domain: 'example.biz') } + + it 'returns the correct records' do + results = described_class.matches_domain('example.com') + + expect(results).to eq([domain]) + end + end + end end diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb index 6a5925b896..9839ee9d4e 100644 --- a/spec/models/domain_block_spec.rb +++ b/spec/models/domain_block_spec.rb @@ -4,11 +4,6 @@ require 'rails_helper' RSpec.describe DomainBlock, type: :model do describe 'validations' do - it 'has a valid fabricator' do - domain_block = Fabricate.build(:domain_block) - expect(domain_block).to be_valid - end - it 'is invalid without a domain' do domain_block = Fabricate.build(:domain_block, domain: nil) domain_block.valid? diff --git a/spec/models/email_domain_block_spec.rb b/spec/models/email_domain_block_spec.rb index 01a7a0f0ed..3321ffc819 100644 --- a/spec/models/email_domain_block_spec.rb +++ b/spec/models/email_domain_block_spec.rb @@ -3,13 +3,6 @@ require 'rails_helper' RSpec.describe EmailDomainBlock, type: :model do - describe 'validations' do - it 'has a valid fabricator' do - email_domain_block = Fabricate.build(:email_domain_block) - expect(email_domain_block).to be_valid - end - end - describe 'block?' do let(:input) { nil } diff --git a/spec/models/extended_description_spec.rb b/spec/models/extended_description_spec.rb new file mode 100644 index 0000000000..ecc27c0f6d --- /dev/null +++ b/spec/models/extended_description_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ExtendedDescription do + describe '.current' do + context 'with the default values' do + it 'makes a new instance' do + record = described_class.current + + expect(record.text).to be_nil + expect(record.updated_at).to be_nil + end + end + + context 'with a custom setting value' do + before do + setting = instance_double(Setting, value: 'Extended text', updated_at: 10.days.ago) + allow(Setting).to receive(:find_by).with(var: 'site_extended_description').and_return(setting) + end + + it 'has the privacy text' do + record = described_class.current + + expect(record.text).to eq('Extended text') + end + end + end +end diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb index f49d585329..a9a9af88ad 100644 --- a/spec/models/follow_spec.rb +++ b/spec/models/follow_spec.rb @@ -9,11 +9,6 @@ RSpec.describe Follow, type: :model do describe 'validations' do subject { Follow.new(account: alice, target_account: bob, rate_limit: true) } - it 'has a valid fabricator' do - follow = Fabricate.build(:follow) - expect(follow).to be_valid - end - it 'is invalid without an account' do follow = Fabricate.build(:follow, account: nil) follow.valid? diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb index 81c75a9641..1c84744138 100644 --- a/spec/models/import_spec.rb +++ b/spec/models/import_spec.rb @@ -23,6 +23,11 @@ RSpec.describe Import, type: :model do 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) diff --git a/spec/models/ip_block_spec.rb b/spec/models/ip_block_spec.rb index 4c4028576a..ed58826672 100644 --- a/spec/models/ip_block_spec.rb +++ b/spec/models/ip_block_spec.rb @@ -2,6 +2,14 @@ require 'rails_helper' -RSpec.describe IpBlock, type: :model do - pending "add some examples to (or delete) #{__FILE__}" +describe IpBlock do + describe 'to_log_human_identifier' do + let(:ip_block) { described_class.new(ip: '192.168.0.1') } + + it 'combines the IP and prefix into a string' do + result = ip_block.to_log_human_identifier + + expect(result).to eq('192.168.0.1/32') + end + end end diff --git a/spec/models/marker_spec.rb b/spec/models/marker_spec.rb index e8561c4c63..51dd584388 100644 --- a/spec/models/marker_spec.rb +++ b/spec/models/marker_spec.rb @@ -2,6 +2,15 @@ require 'rails_helper' -RSpec.describe Marker, type: :model do - pending "add some examples to (or delete) #{__FILE__}" +describe Marker do + describe 'validations' do + describe 'timeline' do + it 'must be included in valid list' do + record = described_class.new(timeline: 'not real timeline') + + expect(record).to_not be_valid + expect(record).to model_have_error_on_field(:timeline) + end + end + end end diff --git a/spec/models/mention_spec.rb b/spec/models/mention_spec.rb index 3de2b4a072..044bb80cf6 100644 --- a/spec/models/mention_spec.rb +++ b/spec/models/mention_spec.rb @@ -4,11 +4,6 @@ require 'rails_helper' RSpec.describe Mention, type: :model do describe 'validations' do - it 'has a valid fabricator' do - mention = Fabricate.build(:mention) - expect(mention).to be_valid - end - it 'is invalid without an account' do mention = Fabricate.build(:mention, account: nil) mention.valid? diff --git a/spec/models/one_time_key_spec.rb b/spec/models/one_time_key_spec.rb index 2a5fe8a9d9..6ff7ffc5c1 100644 --- a/spec/models/one_time_key_spec.rb +++ b/spec/models/one_time_key_spec.rb @@ -2,5 +2,22 @@ require 'rails_helper' -RSpec.describe OneTimeKey, type: :model do +describe OneTimeKey do + describe 'validations' do + context 'with an invalid signature' do + let(:one_time_key) { Fabricate.build(:one_time_key, signature: 'wrong!') } + + it 'is invalid' do + expect(one_time_key).to_not be_valid + end + end + + context 'with an invalid key' do + let(:one_time_key) { Fabricate.build(:one_time_key, key: 'wrong!') } + + it 'is invalid' do + expect(one_time_key).to_not be_valid + end + end + end end diff --git a/spec/models/poll_spec.rb b/spec/models/poll_spec.rb index 474399bf68..8ae04ca41f 100644 --- a/spec/models/poll_spec.rb +++ b/spec/models/poll_spec.rb @@ -2,6 +2,31 @@ require 'rails_helper' -RSpec.describe Poll, type: :model do - pending "add some examples to (or delete) #{__FILE__}" +describe Poll do + describe 'scopes' do + let(:status) { Fabricate(:status) } + let(:attached_poll) { Fabricate(:poll, status: status) } + let(:not_attached_poll) do + Fabricate(:poll).tap do |poll| + poll.status = nil + poll.save(validate: false) + end + end + + describe 'attached' do + it 'finds the correct records' do + results = described_class.attached + + expect(results).to eq([attached_poll]) + end + end + + describe 'unattached' do + it 'finds the correct records' do + results = described_class.unattached + + expect(results).to eq([not_attached_poll]) + end + end + end end diff --git a/spec/models/preview_card_provider_spec.rb b/spec/models/preview_card_provider_spec.rb new file mode 100644 index 0000000000..7425b93946 --- /dev/null +++ b/spec/models/preview_card_provider_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe PreviewCardProvider do + describe 'scopes' do + let(:trendable_and_reviewed) { Fabricate(:preview_card_provider, trendable: true, reviewed_at: 5.days.ago) } + let(:not_trendable_and_not_reviewed) { Fabricate(:preview_card_provider, trendable: false, reviewed_at: nil) } + + describe 'trendable' do + it 'returns the relevant records' do + results = described_class.trendable + + expect(results).to eq([trendable_and_reviewed]) + end + end + + describe 'not_trendable' do + it 'returns the relevant records' do + results = described_class.not_trendable + + expect(results).to eq([not_trendable_and_not_reviewed]) + end + end + + describe 'reviewed' do + it 'returns the relevant records' do + results = described_class.reviewed + + expect(results).to eq([trendable_and_reviewed]) + end + end + + describe 'pending_review' do + it 'returns the relevant records' do + results = described_class.pending_review + + expect(results).to eq([not_trendable_and_not_reviewed]) + end + end + end +end diff --git a/spec/models/privacy_policy_spec.rb b/spec/models/privacy_policy_spec.rb new file mode 100644 index 0000000000..0d74713755 --- /dev/null +++ b/spec/models/privacy_policy_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe PrivacyPolicy do + describe '.current' do + context 'with the default values' do + it 'has the privacy text' do + policy = described_class.current + + expect(policy.text).to eq(PrivacyPolicy::DEFAULT_PRIVACY_POLICY) + end + end + + context 'with a custom setting value' do + before do + terms_setting = instance_double(Setting, value: 'Terms text', updated_at: 10.days.ago) + allow(Setting).to receive(:find_by).with(var: 'site_terms').and_return(terms_setting) + end + + it 'has the privacy text' do + policy = described_class.current + + expect(policy.text).to eq('Terms text') + end + end + end +end diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index d5d40a34f9..20a048c334 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -121,12 +121,6 @@ describe Report do end describe 'validations' do - it 'has a valid fabricator' do - report = Fabricate(:report) - report.valid? - expect(report).to be_valid - end - it 'is invalid if comment is longer than 1000 characters' do report = Fabricate.build(:report, comment: Faker::Lorem.characters(number: 1001)) report.valid? diff --git a/spec/models/rule_spec.rb b/spec/models/rule_spec.rb index d5ec13ddf8..c9b9c55028 100644 --- a/spec/models/rule_spec.rb +++ b/spec/models/rule_spec.rb @@ -2,6 +2,18 @@ require 'rails_helper' -RSpec.describe Rule, type: :model do - pending "add some examples to (or delete) #{__FILE__}" +describe Rule do + describe 'scopes' do + describe 'ordered' do + let(:deleted_rule) { Fabricate(:rule, deleted_at: 10.days.ago) } + let(:first_rule) { Fabricate(:rule, deleted_at: nil, priority: 1) } + let(:last_rule) { Fabricate(:rule, deleted_at: nil, priority: 10) } + + it 'finds the correct records' do + results = described_class.ordered + + expect(results).to eq([first_rule, last_rule]) + end + end + end end diff --git a/spec/models/status_edit_spec.rb b/spec/models/status_edit_spec.rb index 0b9fa70873..2d33514522 100644 --- a/spec/models/status_edit_spec.rb +++ b/spec/models/status_edit_spec.rb @@ -2,6 +2,12 @@ require 'rails_helper' -RSpec.describe StatusEdit, type: :model do - pending "add some examples to (or delete) #{__FILE__}" +describe StatusEdit do + describe '#reblog?' do + it 'returns false' do + record = described_class.new + + expect(record).to_not be_a_reblog + end + end end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 7022c5f006..d1caf267cb 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -114,6 +114,85 @@ RSpec.describe Status, type: :model do end end + describe '#translatable?' do + before do + allow(TranslationService).to receive(:configured?).and_return(true) + allow(TranslationService).to receive(:configured).and_return(TranslationService.new) + allow(TranslationService.configured).to receive(:supported?).with('es', 'en').and_return(true) + + subject.language = 'es' + subject.visibility = :public + end + + context 'all conditions are satisfied' do + it 'returns true' do + expect(subject.translatable?).to be true + end + end + + context 'translation service is not configured' do + it 'returns false' do + allow(TranslationService).to receive(:configured?).and_return(false) + allow(TranslationService).to receive(:configured).and_raise(TranslationService::NotConfiguredError) + expect(subject.translatable?).to be false + end + end + + context 'status language is nil' do + it 'returns true' do + subject.language = nil + allow(TranslationService.configured).to receive(:supported?).with(nil, 'en').and_return(true) + expect(subject.translatable?).to be true + end + end + + context 'status language is same as default locale' do + it 'returns false' do + subject.language = I18n.locale + expect(subject.translatable?).to be false + end + end + + context 'status language is unsupported' do + it 'returns false' do + subject.language = 'af' + allow(TranslationService.configured).to receive(:supported?).with('af', 'en').and_return(false) + expect(subject.translatable?).to be false + end + end + + context 'default locale is unsupported' do + it 'returns false' do + allow(TranslationService.configured).to receive(:supported?).with('es', 'af').and_return(false) + I18n.with_locale('af') do + expect(subject.translatable?).to be false + end + end + end + + context 'default locale has region' do + it 'returns true' do + I18n.with_locale('en-GB') do + expect(subject.translatable?).to be true + end + end + end + + context 'status text is blank' do + it 'returns false' do + subject.text = ' ' + expect(subject.translatable?).to be false + end + end + + context 'status visiblity is hidden' do + it 'returns false' do + subject.visibility = 'limited' + expect(subject.translatable?).to be false + end + end + end + describe '#content' do it 'returns the text of the status if it is not a reblog' do expect(subject.content).to eql subject.text diff --git a/spec/models/trends/tags_spec.rb b/spec/models/trends/tags_spec.rb index a9473e15c4..09ac918d00 100644 --- a/spec/models/trends/tags_spec.rb +++ b/spec/models/trends/tags_spec.rb @@ -24,7 +24,9 @@ RSpec.describe Trends::Tags do end describe '#query' do - pending + it 'returns a composable query scope' do + expect(subject.query).to be_a Trends::Query + end end describe '#refresh' do diff --git a/spec/policies/account_policy_spec.rb b/spec/policies/account_policy_spec.rb index 0f23fd97e2..d961532332 100644 --- a/spec/policies/account_policy_spec.rb +++ b/spec/policies/account_policy_spec.rb @@ -116,4 +116,44 @@ RSpec.describe AccountPolicy do end end end + + permissions :review? do + context 'admin' do + it 'permits' do + expect(subject).to permit(admin) + end + end + + context 'not admin' do + it 'denies' do + expect(subject).to_not permit(john) + end + end + end + + permissions :destroy? do + context 'admin' do + context 'with a temporarily suspended account' do + before { allow(alice).to receive(:suspended_temporarily?).and_return(true) } + + it 'permits' do + expect(subject).to permit(admin, alice) + end + end + + context 'with a not temporarily suspended account' do + before { allow(alice).to receive(:suspended_temporarily?).and_return(false) } + + it 'denies' do + expect(subject).to_not permit(admin, alice) + end + end + end + + context 'not admin' do + it 'denies' do + expect(subject).to_not permit(john, alice) + end + end + end end diff --git a/spec/policies/account_warning_preset_policy_spec.rb b/spec/policies/account_warning_preset_policy_spec.rb new file mode 100644 index 0000000000..63bf33de24 --- /dev/null +++ b/spec/policies/account_warning_preset_policy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +describe AccountWarningPresetPolicy do + let(:policy) { described_class } + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:john) { Fabricate(:account) } + + permissions :index?, :create?, :update?, :destroy? do + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, Tag) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, Tag) + end + end + end +end diff --git a/spec/policies/admin/status_policy_spec.rb b/spec/policies/admin/status_policy_spec.rb new file mode 100644 index 0000000000..9e81a4f5f1 --- /dev/null +++ b/spec/policies/admin/status_policy_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +describe Admin::StatusPolicy do + let(:policy) { described_class } + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:john) { Fabricate(:account) } + let(:status) { Fabricate(:status) } + + permissions :index?, :update?, :review?, :destroy? do + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, Tag) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, Tag) + end + end + end + + permissions :show? do + context 'with an admin' do + context 'with a public visible status' do + before { allow(status).to receive(:public_visibility?).and_return(true) } + + it 'permits' do + expect(policy).to permit(admin, status) + end + end + + context 'with a not public visible status' do + before { allow(status).to receive(:public_visibility?).and_return(false) } + + it 'denies' do + expect(policy).to_not permit(admin, status) + end + end + end + + context 'with a non admin' do + it 'denies' do + expect(policy).to_not permit(john, status) + end + end + end +end diff --git a/spec/policies/announcement_policy_spec.rb b/spec/policies/announcement_policy_spec.rb new file mode 100644 index 0000000000..3d230b3cb4 --- /dev/null +++ b/spec/policies/announcement_policy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +describe AnnouncementPolicy do + let(:policy) { described_class } + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:john) { Fabricate(:account) } + + permissions :index?, :create?, :update?, :destroy? do + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, Tag) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, Tag) + end + end + end +end diff --git a/spec/policies/appeal_policy_spec.rb b/spec/policies/appeal_policy_spec.rb new file mode 100644 index 0000000000..d7498eb9f0 --- /dev/null +++ b/spec/policies/appeal_policy_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +describe AppealPolicy do + let(:policy) { described_class } + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:john) { Fabricate(:account) } + let(:appeal) { Fabricate(:appeal) } + + permissions :index? do + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, Tag) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, Tag) + end + end + end + + permissions :reject? do + context 'with an admin' do + context 'with a pending appeal' do + before { allow(appeal).to receive(:pending?).and_return(true) } + + it 'permits' do + expect(policy).to permit(admin, appeal) + end + end + + context 'with a not pending appeal' do + before { allow(appeal).to receive(:pending?).and_return(false) } + + it 'denies' do + expect(policy).to_not permit(admin, appeal) + end + end + end + + context 'with a non admin' do + it 'denies' do + expect(policy).to_not permit(john, appeal) + end + end + end +end diff --git a/spec/policies/canonical_email_block_policy_spec.rb b/spec/policies/canonical_email_block_policy_spec.rb new file mode 100644 index 0000000000..0e55febfa9 --- /dev/null +++ b/spec/policies/canonical_email_block_policy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +describe CanonicalEmailBlockPolicy do + let(:policy) { described_class } + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:john) { Fabricate(:account) } + + permissions :index?, :show?, :test?, :create?, :destroy? do + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, Tag) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, Tag) + end + end + end +end diff --git a/spec/policies/delivery_policy_spec.rb b/spec/policies/delivery_policy_spec.rb new file mode 100644 index 0000000000..fbcbf390d7 --- /dev/null +++ b/spec/policies/delivery_policy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +describe DeliveryPolicy do + let(:policy) { described_class } + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:john) { Fabricate(:account) } + + permissions :clear_delivery_errors?, :restart_delivery?, :stop_delivery? do + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, Tag) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, Tag) + end + end + end +end diff --git a/spec/policies/email_domain_block_policy_spec.rb b/spec/policies/email_domain_block_policy_spec.rb index 913075c3d2..e7c455907a 100644 --- a/spec/policies/email_domain_block_policy_spec.rb +++ b/spec/policies/email_domain_block_policy_spec.rb @@ -8,7 +8,7 @@ RSpec.describe EmailDomainBlockPolicy do let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } let(:john) { Fabricate(:account) } - permissions :index?, :create?, :destroy? do + permissions :index?, :show?, :create?, :destroy? do context 'admin' do it 'permits' do expect(subject).to permit(admin, EmailDomainBlock) diff --git a/spec/policies/follow_recommendation_policy_spec.rb b/spec/policies/follow_recommendation_policy_spec.rb new file mode 100644 index 0000000000..01f4da0be2 --- /dev/null +++ b/spec/policies/follow_recommendation_policy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +describe FollowRecommendationPolicy do + let(:policy) { described_class } + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:john) { Fabricate(:account) } + + permissions :show?, :suppress?, :unsuppress? do + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, Tag) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, Tag) + end + end + end +end diff --git a/spec/policies/ip_block_policy_spec.rb b/spec/policies/ip_block_policy_spec.rb new file mode 100644 index 0000000000..3cfa85863c --- /dev/null +++ b/spec/policies/ip_block_policy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +describe IpBlockPolicy do + let(:policy) { described_class } + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:john) { Fabricate(:account) } + + permissions :index?, :show?, :create?, :update?, :destroy? do + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, Tag) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, Tag) + end + end + end +end diff --git a/spec/policies/preview_card_policy_spec.rb b/spec/policies/preview_card_policy_spec.rb new file mode 100644 index 0000000000..d6675c5b34 --- /dev/null +++ b/spec/policies/preview_card_policy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +describe PreviewCardPolicy do + let(:policy) { described_class } + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:john) { Fabricate(:account) } + + permissions :index?, :review? do + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, Tag) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, Tag) + end + end + end +end diff --git a/spec/policies/preview_card_provider_policy_spec.rb b/spec/policies/preview_card_provider_policy_spec.rb new file mode 100644 index 0000000000..8d3715de95 --- /dev/null +++ b/spec/policies/preview_card_provider_policy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +describe PreviewCardProviderPolicy do + let(:policy) { described_class } + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:john) { Fabricate(:account) } + + permissions :index?, :review? do + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, Tag) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, Tag) + end + end + end +end diff --git a/spec/policies/rule_policy_spec.rb b/spec/policies/rule_policy_spec.rb new file mode 100644 index 0000000000..0e45f6df02 --- /dev/null +++ b/spec/policies/rule_policy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +describe RulePolicy do + let(:policy) { described_class } + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:john) { Fabricate(:account) } + + permissions :index?, :create?, :update?, :destroy? do + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, Tag) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, Tag) + end + end + end +end diff --git a/spec/policies/settings_policy_spec.rb b/spec/policies/settings_policy_spec.rb index e16ee51a48..3268c16225 100644 --- a/spec/policies/settings_policy_spec.rb +++ b/spec/policies/settings_policy_spec.rb @@ -8,7 +8,7 @@ RSpec.describe SettingsPolicy do let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } let(:john) { Fabricate(:account) } - permissions :update?, :show? do + permissions :update?, :show?, :destroy? do context 'admin?' do it 'permits' do expect(subject).to permit(admin, Settings) diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb index 2afcfe96e0..9a30aef3c5 100644 --- a/spec/policies/status_policy_spec.rb +++ b/spec/policies/status_policy_spec.rb @@ -39,6 +39,14 @@ RSpec.describe StatusPolicy, type: :model do expect(subject).to permit(alice, status) end + it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do + status.visibility = :direct + status.mentions = [Fabricate(:mention, account: bob)] + status.mentions.load + + expect(subject).to permit(bob, status) + end + it 'denies access when direct and viewer is not mentioned' do viewer = Fabricate(:account) status.visibility = :direct diff --git a/spec/policies/tag_policy_spec.rb b/spec/policies/tag_policy_spec.rb index 9be7140fc2..fb09fdd3be 100644 --- a/spec/policies/tag_policy_spec.rb +++ b/spec/policies/tag_policy_spec.rb @@ -8,7 +8,7 @@ RSpec.describe TagPolicy do let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } let(:john) { Fabricate(:account) } - permissions :index?, :show?, :update? do + permissions :index?, :show?, :update?, :review? do context 'staff?' do it 'permits' do expect(subject).to permit(admin, Tag) diff --git a/spec/policies/webhook_policy_spec.rb b/spec/policies/webhook_policy_spec.rb new file mode 100644 index 0000000000..1eac8932d4 --- /dev/null +++ b/spec/policies/webhook_policy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +describe WebhookPolicy do + let(:policy) { described_class } + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } + let(:john) { Fabricate(:account) } + + permissions :index?, :create?, :show?, :update?, :enable?, :disable?, :rotate_secret?, :destroy? do + context 'with an admin' do + it 'permits' do + expect(policy).to permit(admin, Tag) + end + end + + context 'with a non-admin' do + it 'denies' do + expect(policy).to_not permit(john, Tag) + end + end + end +end diff --git a/spec/validators/email_mx_validator_spec.rb b/spec/validators/email_mx_validator_spec.rb index ffb6851d09..a11b8e01e0 100644 --- a/spec/validators/email_mx_validator_spec.rb +++ b/spec/validators/email_mx_validator_spec.rb @@ -41,6 +41,22 @@ describe EmailMxValidator do expect(user.errors).to_not have_received(:add) end + it 'adds an error if the TagManager fails to normalize domain' do + double = instance_double(TagManager) + allow(TagManager).to receive(:instance).and_return(double) + allow(double).to receive(:normalize_domain).with('example.com').and_raise(Addressable::URI::InvalidURIError) + + user = double(email: 'foo@example.com', errors: double(add: nil)) + subject.validate(user) + expect(user.errors).to have_received(:add) + end + + it 'adds an error if the domain email portion is blank' do + user = double(email: 'foo@', errors: double(add: nil)) + subject.validate(user) + expect(user.errors).to have_received(:add) + end + it 'adds an error if the email domain name contains empty labels' do resolver = double diff --git a/spec/workers/admin/account_deletion_worker_spec.rb b/spec/workers/admin/account_deletion_worker_spec.rb new file mode 100644 index 0000000000..631cab6648 --- /dev/null +++ b/spec/workers/admin/account_deletion_worker_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::AccountDeletionWorker do + let(:worker) { described_class.new } + + describe 'perform' do + let(:account) { Fabricate(:account) } + let(:service) { instance_double(DeleteAccountService, call: true) } + + it 'calls delete account service' do + allow(DeleteAccountService).to receive(:new).and_return(service) + worker.perform(account.id) + + expect(service).to have_received(:call).with(account, { reserve_email: true, reserve_username: true }) + end + end +end diff --git a/spec/workers/cache_buster_worker_spec.rb b/spec/workers/cache_buster_worker_spec.rb new file mode 100644 index 0000000000..adeb287fa3 --- /dev/null +++ b/spec/workers/cache_buster_worker_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe CacheBusterWorker do + let(:worker) { described_class.new } + + describe 'perform' do + let(:path) { 'https://example.com' } + let(:service) { instance_double(CacheBuster, bust: true) } + + it 'calls the cache buster' do + allow(CacheBuster).to receive(:new).and_return(service) + worker.perform(path) + + expect(service).to have_received(:bust).with(path) + end + end +end diff --git a/spec/workers/poll_expiration_notify_worker_spec.rb b/spec/workers/poll_expiration_notify_worker_spec.rb new file mode 100644 index 0000000000..8229db815d --- /dev/null +++ b/spec/workers/poll_expiration_notify_worker_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe PollExpirationNotifyWorker do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error for missing record' do + expect { worker.perform(nil) }.to_not raise_error + end + end +end diff --git a/spec/workers/post_process_media_worker_spec.rb b/spec/workers/post_process_media_worker_spec.rb new file mode 100644 index 0000000000..33072704bf --- /dev/null +++ b/spec/workers/post_process_media_worker_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe PostProcessMediaWorker do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error for missing record' do + expect { worker.perform(nil) }.to_not raise_error + end + end +end diff --git a/spec/workers/push_conversation_worker_spec.rb b/spec/workers/push_conversation_worker_spec.rb new file mode 100644 index 0000000000..5fbb4c6853 --- /dev/null +++ b/spec/workers/push_conversation_worker_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe PushConversationWorker do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error for missing record' do + expect { worker.perform(nil) }.to_not raise_error + end + end +end diff --git a/spec/workers/push_encrypted_message_worker_spec.rb b/spec/workers/push_encrypted_message_worker_spec.rb new file mode 100644 index 0000000000..3cd04ce7b4 --- /dev/null +++ b/spec/workers/push_encrypted_message_worker_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe PushEncryptedMessageWorker do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error for missing record' do + expect { worker.perform(nil) }.to_not raise_error + end + end +end diff --git a/spec/workers/push_update_worker_spec.rb b/spec/workers/push_update_worker_spec.rb new file mode 100644 index 0000000000..c8f94fa82a --- /dev/null +++ b/spec/workers/push_update_worker_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe PushUpdateWorker do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error for missing record' do + account_id = nil + status_id = nil + + expect { worker.perform(account_id, status_id) }.to_not raise_error + end + end +end diff --git a/spec/workers/redownload_avatar_worker_spec.rb b/spec/workers/redownload_avatar_worker_spec.rb new file mode 100644 index 0000000000..b44ae9f035 --- /dev/null +++ b/spec/workers/redownload_avatar_worker_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe RedownloadAvatarWorker do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error for missing record' do + expect { worker.perform(nil) }.to_not raise_error + end + end +end diff --git a/spec/workers/redownload_header_worker_spec.rb b/spec/workers/redownload_header_worker_spec.rb new file mode 100644 index 0000000000..767ae7a5ab --- /dev/null +++ b/spec/workers/redownload_header_worker_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe RedownloadHeaderWorker do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error for missing record' do + expect { worker.perform(nil) }.to_not raise_error + end + end +end diff --git a/spec/workers/remove_featured_tag_worker_spec.rb b/spec/workers/remove_featured_tag_worker_spec.rb new file mode 100644 index 0000000000..a64bd0605f --- /dev/null +++ b/spec/workers/remove_featured_tag_worker_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe RemoveFeaturedTagWorker do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error for missing record' do + account_id = nil + featured_tag_id = nil + expect { worker.perform(account_id, featured_tag_id) }.to_not raise_error + end + end +end diff --git a/spec/workers/resolve_account_worker_spec.rb b/spec/workers/resolve_account_worker_spec.rb new file mode 100644 index 0000000000..6f3cff099f --- /dev/null +++ b/spec/workers/resolve_account_worker_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ResolveAccountWorker do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error for missing record' do + expect { worker.perform(nil) }.to_not raise_error + end + end +end diff --git a/spec/workers/scheduler/indexing_scheduler_spec.rb b/spec/workers/scheduler/indexing_scheduler_spec.rb new file mode 100644 index 0000000000..568f0fc84d --- /dev/null +++ b/spec/workers/scheduler/indexing_scheduler_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Scheduler::IndexingScheduler do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error' do + expect { worker.perform }.to_not raise_error + end + end +end diff --git a/spec/workers/scheduler/instance_refresh_scheduler_spec.rb b/spec/workers/scheduler/instance_refresh_scheduler_spec.rb new file mode 100644 index 0000000000..8f686a6998 --- /dev/null +++ b/spec/workers/scheduler/instance_refresh_scheduler_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Scheduler::InstanceRefreshScheduler do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error' do + expect { worker.perform }.to_not raise_error + end + end +end diff --git a/spec/workers/scheduler/ip_cleanup_scheduler_spec.rb b/spec/workers/scheduler/ip_cleanup_scheduler_spec.rb new file mode 100644 index 0000000000..50af030117 --- /dev/null +++ b/spec/workers/scheduler/ip_cleanup_scheduler_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Scheduler::IpCleanupScheduler do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error' do + expect { worker.perform }.to_not raise_error + end + end +end diff --git a/spec/workers/scheduler/pghero_scheduler_spec.rb b/spec/workers/scheduler/pghero_scheduler_spec.rb new file mode 100644 index 0000000000..e404e5fe47 --- /dev/null +++ b/spec/workers/scheduler/pghero_scheduler_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Scheduler::PgheroScheduler do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error' do + expect { worker.perform }.to_not raise_error + end + end +end diff --git a/spec/workers/scheduler/scheduled_statuses_scheduler_spec.rb b/spec/workers/scheduler/scheduled_statuses_scheduler_spec.rb new file mode 100644 index 0000000000..13c853c62a --- /dev/null +++ b/spec/workers/scheduler/scheduled_statuses_scheduler_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Scheduler::ScheduledStatusesScheduler do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error' do + expect { worker.perform }.to_not raise_error + end + end +end diff --git a/spec/workers/scheduler/suspended_user_cleanup_scheduler_spec.rb b/spec/workers/scheduler/suspended_user_cleanup_scheduler_spec.rb new file mode 100644 index 0000000000..25f0e1fce4 --- /dev/null +++ b/spec/workers/scheduler/suspended_user_cleanup_scheduler_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Scheduler::SuspendedUserCleanupScheduler do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error' do + expect { worker.perform }.to_not raise_error + end + end +end diff --git a/spec/workers/scheduler/trends/refresh_scheduler_spec.rb b/spec/workers/scheduler/trends/refresh_scheduler_spec.rb new file mode 100644 index 0000000000..c0c5f032bf --- /dev/null +++ b/spec/workers/scheduler/trends/refresh_scheduler_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Scheduler::Trends::RefreshScheduler do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error' do + expect { worker.perform }.to_not raise_error + end + end +end diff --git a/spec/workers/scheduler/trends/review_notifications_scheduler_spec.rb b/spec/workers/scheduler/trends/review_notifications_scheduler_spec.rb new file mode 100644 index 0000000000..cc971c24b4 --- /dev/null +++ b/spec/workers/scheduler/trends/review_notifications_scheduler_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Scheduler::Trends::ReviewNotificationsScheduler do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error' do + expect { worker.perform }.to_not raise_error + end + end +end diff --git a/spec/workers/scheduler/vacuum_scheduler_spec.rb b/spec/workers/scheduler/vacuum_scheduler_spec.rb new file mode 100644 index 0000000000..36ecc93d8e --- /dev/null +++ b/spec/workers/scheduler/vacuum_scheduler_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Scheduler::VacuumScheduler do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error' do + expect { worker.perform }.to_not raise_error + end + end +end diff --git a/spec/workers/unpublish_announcement_worker_spec.rb b/spec/workers/unpublish_announcement_worker_spec.rb new file mode 100644 index 0000000000..c742c30bce --- /dev/null +++ b/spec/workers/unpublish_announcement_worker_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe UnpublishAnnouncementWorker do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error for missing record' do + expect { worker.perform(nil) }.to_not raise_error + end + end +end diff --git a/spec/workers/webhooks/delivery_worker_spec.rb b/spec/workers/webhooks/delivery_worker_spec.rb new file mode 100644 index 0000000000..daf8a3e285 --- /dev/null +++ b/spec/workers/webhooks/delivery_worker_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Webhooks::DeliveryWorker do + let(:worker) { described_class.new } + + describe 'perform' do + it 'runs without error' do + expect { worker.perform(nil, nil) }.to_not raise_error + end + end +end diff --git a/streaming/index.js b/streaming/index.js index 8ee19ae70d..851dc72ea3 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -7,6 +7,7 @@ const express = require('express'); const http = require('http'); const redis = require('redis'); const pg = require('pg'); +const dbUrlToConfig = require('pg-connection-string').parse; const log = require('npmlog'); const url = require('url'); const uuid = require('uuid'); @@ -23,43 +24,6 @@ dotenv.config({ log.level = process.env.LOG_LEVEL || 'verbose'; -/** - * @param {string} dbUrl - * @return {Object.} - */ -const dbUrlToConfig = (dbUrl) => { - if (!dbUrl) { - return {}; - } - - const params = url.parse(dbUrl, true); - const config = {}; - - if (params.auth) { - [config.user, config.password] = params.auth.split(':'); - } - - if (params.hostname) { - config.host = params.hostname; - } - - if (params.port) { - config.port = params.port; - } - - if (params.pathname) { - config.database = params.pathname.split('/')[1]; - } - - const ssl = params.query && params.query.ssl; - - if (ssl && ssl === 'true' || ssl === '1') { - config.ssl = true; - } - - return config; -}; - /** * @param {Object.} defaultConfig * @param {string} redisUrl @@ -117,9 +81,10 @@ const startMaster = () => { log.warn(`Starting streaming API server master with ${numWorkers} workers`); }; -const startWorker = async (workerId) => { - log.warn(`Starting worker ${workerId}`); - +/** + * @return {Object.} + */ +const pgConfigFromEnv = () => { const pgConfigs = { development: { user: process.env.DB_USER || pg.defaults.user, @@ -138,16 +103,45 @@ const startWorker = async (workerId) => { }, }; + let baseConfig; + + if (process.env.DATABASE_URL) { + baseConfig = dbUrlToConfig(process.env.DATABASE_URL); + } else { + baseConfig = pgConfigs[env]; + + if (process.env.DB_SSLMODE) { + switch(process.env.DB_SSLMODE) { + case 'disable': + case '': + baseConfig.ssl = false; + break; + case 'no-verify': + baseConfig.ssl = { rejectUnauthorized: false }; + break; + default: + baseConfig.ssl = {}; + break; + } + } + } + + return { + ...baseConfig, + max: process.env.DB_POOL || 10, + connectionTimeoutMillis: 15000, + application_name: '', + }; +}; + +const startWorker = async (workerId) => { + log.warn(`Starting worker ${workerId}`); + const app = express(); app.set('trust proxy', process.env.TRUSTED_PROXY_IP ? process.env.TRUSTED_PROXY_IP.split(/(?:\s*,\s*|\s+)/) : 'loopback,uniquelocal'); - const pgPool = new pg.Pool(Object.assign(pgConfigs[env], dbUrlToConfig(process.env.DATABASE_URL), { - max: process.env.DB_POOL || 10, - connectionTimeoutMillis: 15000, - ssl: !!process.env.DB_SSLMODE && process.env.DB_SSLMODE !== 'disable', - })); - + const pgPool = new pg.Pool(pgConfigFromEnv()); const server = http.createServer(app); const redisNamespace = process.env.REDIS_NAMESPACE || null; diff --git a/yarn.lock b/yarn.lock index 9c7e3827ad..ddb5a7c116 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8332,6 +8332,11 @@ pg-connection-string@^2.4.0: resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.4.0.tgz#c979922eb47832999a204da5dbe1ebf2341b6a10" integrity sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ== +pg-connection-string@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" + integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== + pg-int8@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"