diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index c6dcc4d46a..0000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# For details, see https://github.com/devcontainers/images/tree/main/src/ruby -FROM mcr.microsoft.com/devcontainers/ruby:1-3.3-bookworm - -# Install node version from .nvmrc -WORKDIR /app -COPY .nvmrc . -RUN /bin/bash --login -i -c "nvm install" - -# Install additional OS packages -RUN apt-get update && \ - export DEBIAN_FRONTEND=noninteractive && \ - apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libvips42 libpam-dev - -# Move welcome message to where VS Code expects it -COPY .devcontainer/welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt diff --git a/.devcontainer/codespaces/devcontainer.json b/.devcontainer/codespaces/devcontainer.json deleted file mode 100644 index 8acffec825..0000000000 --- a/.devcontainer/codespaces/devcontainer.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "Mastodon on GitHub Codespaces", - "dockerComposeFile": "../compose.yaml", - "service": "app", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - - "features": { - "ghcr.io/devcontainers/features/sshd:1": {} - }, - - "runServices": ["app", "db", "redis"], - - "forwardPorts": [3000, 4000], - - "portsAttributes": { - "3000": { - "label": "web", - "onAutoForward": "notify" - }, - "4000": { - "label": "stream", - "onAutoForward": "silent" - } - }, - - "remoteUser": "root", - - "otherPortsAttributes": { - "onAutoForward": "silent" - }, - - "remoteEnv": { - "LOCAL_DOMAIN": "${localEnv:CODESPACE_NAME}-3000.app.github.dev", - "LOCAL_HTTPS": "true", - "STREAMING_API_BASE_URL": "https://${localEnv:CODESPACE_NAME}-4000.app.github.dev", - "DISABLE_FORGERY_REQUEST_PROTECTION": "true", - "ES_ENABLED": "", - "LIBRE_TRANSLATE_ENDPOINT": "" - }, - - "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", - "postCreateCommand": "COREPACK_ENABLE_DOWNLOAD_PROMPT=0 bin/setup", - "waitFor": "postCreateCommand", - - "customizations": { - "vscode": { - "settings": {}, - "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] - } - } -} diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml deleted file mode 100644 index 1e2e1ba7de..0000000000 --- a/.devcontainer/compose.yaml +++ /dev/null @@ -1,89 +0,0 @@ -services: - app: - working_dir: /workspaces/mastodon/ - build: - context: .. - dockerfile: .devcontainer/Dockerfile - volumes: - - ..:/workspaces/mastodon:cached - environment: - RAILS_ENV: development - NODE_ENV: development - BIND: 0.0.0.0 - REDIS_HOST: redis - REDIS_PORT: '6379' - DB_HOST: db - DB_USER: postgres - DB_PASS: postgres - DB_PORT: '5432' - ES_ENABLED: 'true' - ES_HOST: es - ES_PORT: '9200' - LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000 - # Overrides default command so things don't shut down after the process ends. - command: sleep infinity - ports: - - '127.0.0.1:3000:3000' - - '127.0.0.1:3035:3035' - - '127.0.0.1:4000:4000' - networks: - - external_network - - internal_network - - db: - image: postgres:14-alpine - restart: unless-stopped - volumes: - - postgres-data:/var/lib/postgresql/data - environment: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_HOST_AUTH_METHOD: trust - networks: - - internal_network - - redis: - image: redis:7-alpine - restart: unless-stopped - volumes: - - redis-data:/data - networks: - - internal_network - - es: - image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2 - restart: unless-stopped - environment: - ES_JAVA_OPTS: -Xms512m -Xmx512m - cluster.name: es-mastodon - discovery.type: single-node - bootstrap.memory_lock: 'true' - volumes: - - es-data:/usr/share/elasticsearch/data - networks: - - internal_network - ulimits: - memlock: - soft: -1 - hard: -1 - - libretranslate: - image: libretranslate/libretranslate:v1.5.7 - restart: unless-stopped - volumes: - - lt-data:/home/libretranslate/.local - networks: - - external_network - - internal_network - -volumes: - postgres-data: - redis-data: - es-data: - lt-data: - -networks: - external_network: - internal_network: - internal: true diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index fb88f7801f..0000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "Mastodon on local machine", - "dockerComposeFile": "compose.yaml", - "service": "app", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - - "features": { - "ghcr.io/devcontainers/features/sshd:1": {} - }, - - "forwardPorts": [3000, 4000], - - "portsAttributes": { - "3000": { - "label": "web", - "onAutoForward": "notify", - "requireLocalPort": true - }, - "4000": { - "label": "stream", - "onAutoForward": "silent", - "requireLocalPort": true - } - }, - - "remoteUser": "root", - - "otherPortsAttributes": { - "onAutoForward": "silent" - }, - - "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", - "postCreateCommand": "bin/setup", - "waitFor": "postCreateCommand", - - "customizations": { - "vscode": { - "settings": {}, - "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] - } - } -} diff --git a/.devcontainer/welcome-message.txt b/.devcontainer/welcome-message.txt deleted file mode 100644 index dbc19c910c..0000000000 --- a/.devcontainer/welcome-message.txt +++ /dev/null @@ -1,7 +0,0 @@ -👋 Welcome to your Mastodon Dev Container! - -🛠️ Your environment is fully setup with all the required software. - -💥 Run `bin/dev` to start the application processes. - -🥼 Run `RAILS_ENV=test bin/rails assets:precompile && RAILS_ENV=test bin/rspec` to run the test suite. diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 41da718049..0000000000 --- a/.dockerignore +++ /dev/null @@ -1,22 +0,0 @@ -.bundle -.env -.env.* -.git -.gitattributes -.gitignore -.github -public/system -public/assets -public/packs -public/packs-test -node_modules -neo4j -vendor/bundle -.DS_Store -*.swp -*~ -postgres -postgres14 -redis -elasticsearch -chart diff --git a/.env.development b/.env.development deleted file mode 100644 index 0330da8377..0000000000 --- a/.env.development +++ /dev/null @@ -1,4 +0,0 @@ -# Required by ActiveRecord encryption feature -ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fkSxKD2bF396kdQbrP1EJ7WbU7ZgNokR -ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=r0hvVmzBVsjxC7AMlwhOzmtc36ZCOS1E -ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=PhdFyyfy5xJ7WVd2lWBpcPScRQHzRTNr diff --git a/.env.production.sample b/.env.production.sample deleted file mode 100644 index fa75a70c57..0000000000 --- a/.env.production.sample +++ /dev/null @@ -1,311 +0,0 @@ -# This is a sample configuration file. You can generate your configuration -# with the `bundle exec rails mastodon:setup` interactive setup wizard, but to customize -# your setup even further, you'll need to edit it manually. This sample does -# not demonstrate all available configuration options. Please look at -# https://docs.joinmastodon.org/admin/config/ for the full documentation. - -# Note that this file accepts slightly different syntax depending on whether -# you are using `docker-compose` or not. In particular, if you use -# `docker-compose`, the value of each declared variable will be taken verbatim, -# including surrounding quotes. -# See: https://github.com/mastodon/mastodon/issues/16895 - -# Federation -# ---------- -# This identifies your server and cannot be changed safely later -# ---------- -LOCAL_DOMAIN=example.com - -# Use this only if you need to run mastodon on a different domain than the one used for federation. -# You can read more about this option on https://docs.joinmastodon.org/admin/config/#web-domain -# DO *NOT* USE THIS UNLESS YOU KNOW *EXACTLY* WHAT YOU ARE DOING. -# WEB_DOMAIN=mastodon.example.com - -# Use this if you want to have several aliases handler@example1.com -# handler@example2.com etc. for the same user. LOCAL_DOMAIN should not -# be added. Comma separated values -# ALTERNATE_DOMAINS=example1.com,example2.com - -# Use HTTP proxy for outgoing request (optional) -# http_proxy=http://gateway.local:8118 -# Access control for hidden service. -# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true - -# Authorized fetch mode (optional) -# Require remote servers to authentify when fetching toots, see -# https://docs.joinmastodon.org/admin/config/#authorized_fetch -# AUTHORIZED_FETCH=true - -# Limited federation mode (optional) -# Only allow federation with specific domains, see -# https://docs.joinmastodon.org/admin/config/#whitelist_mode -# LIMITED_FEDERATION_MODE=true - -# Redis -# ----- -REDIS_HOST=localhost -REDIS_PORT=6379 - - -# PostgreSQL -# ---------- -DB_HOST=/var/run/postgresql -DB_USER=mastodon -DB_NAME=mastodon_production -DB_PASS= -DB_PORT=5432 - - -# Elasticsearch (optional) -# ------------------------ -#ES_ENABLED=true -#ES_HOST=localhost -#ES_PORT=9200 -# Authentication for ES (optional) -#ES_USER=elastic -#ES_PASS=password - - -# Secrets -# ------- -# Generate each with the `RAILS_ENV=production bundle exec rails secret` task (`docker-compose run --rm web bundle exec rails secret` if you use docker compose) -# ------- -SECRET_KEY_BASE= -OTP_SECRET= - - -# Web Push -# -------- -# Generate with `bundle exec rails mastodon:webpush:generate_vapid_key` (first is the private key, second is the public one) -# You should only generate this once per instance. If you later decide to change it, all push subscription will -# be invalidated, requiring the users to access the website again to resubscribe. -# -------- -VAPID_PRIVATE_KEY= -VAPID_PUBLIC_KEY= - - -# Registrations -# ------------- - -# Single user mode will disable registrations and redirect frontpage to the first profile -# SINGLE_USER_MODE=true - -# Prevent registrations with following e-mail domains -# EMAIL_DOMAIN_DENYLIST=example1.com|example2.de|etc - -# Only allow registrations with the following e-mail domains -# EMAIL_DOMAIN_ALLOWLIST=example1.com|example2.de|etc - -#TODO move this -# Optionally change default language -# DEFAULT_LOCALE=de - - -# Sending mail -# ------------ -SMTP_SERVER= -SMTP_PORT=587 -SMTP_LOGIN= -SMTP_PASSWORD= -SMTP_FROM_ADDRESS=notifications@example.com - - -# File storage (optional) -# ----------------------- -# The attachment host must allow cross origin request from WEB_DOMAIN or -# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the -# following header field: -# Access-Control-Allow-Origin: https://192.168.1.123:9000/ -# ----------------------- -#S3_ENABLED=true -#S3_BUCKET=files.example.com -#AWS_ACCESS_KEY_ID= -#AWS_SECRET_ACCESS_KEY= -#S3_ALIAS_HOST=files.example.com - -# Swift (optional) -# The attachment host must allow cross origin request - see the description -# above. -# SWIFT_ENABLED=true -# SWIFT_USERNAME= -# For Keystone V3, the value for SWIFT_TENANT should be the project name -# SWIFT_TENANT= -# SWIFT_PASSWORD= -# Some OpenStack V3 providers require PROJECT_ID (optional) -# SWIFT_PROJECT_ID= -# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid -# issues with token rate-limiting during high load. -# SWIFT_AUTH_URL= -# SWIFT_CONTAINER= -# SWIFT_OBJECT_URL= -# SWIFT_REGION= -# Defaults to 'default' -# SWIFT_DOMAIN_NAME= -# Defaults to 60 seconds. Set to 0 to disable -# SWIFT_CACHE_TTL= - -# Optional asset host for multi-server setups -# The asset host must allow cross origin request from WEB_DOMAIN or LOCAL_DOMAIN -# if WEB_DOMAIN is not set. For example, the server may have the -# following header field: -# Access-Control-Allow-Origin: https://example.com/ -# CDN_HOST=https://assets.example.com - -# Optional list of hosts that are allowed to serve media for your instance -# This is useful if you include external media in your custom CSS or about page, -# or if your data storage provider makes use of redirects to other domains. -# EXTRA_DATA_HOSTS=https://data.example1.com|https://data.example2.com - -# Optional alias for S3 (e.g. to serve files on a custom domain, possibly using Cloudfront or Cloudflare) -# S3_ALIAS_HOST= - -# Streaming API integration -# STREAMING_API_BASE_URL= - - -# External authentication (optional) -# ---------------------------------- -# LDAP authentication (optional) -# LDAP_ENABLED=true -# LDAP_HOST=localhost -# LDAP_PORT=389 -# LDAP_METHOD=simple_tls -# LDAP_BASE= -# LDAP_BIND_DN= -# LDAP_PASSWORD= -# LDAP_UID=cn -# LDAP_MAIL=mail -# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email})) -# LDAP_UID_CONVERSION_ENABLED=true -# LDAP_UID_CONVERSION_SEARCH=., - -# LDAP_UID_CONVERSION_REPLACE=_ - -# PAM authentication (optional) -# PAM authentication uses for the email generation the "email" pam variable -# and optional as fallback PAM_DEFAULT_SUFFIX -# The pam environment variable "email" is provided by: -# https://github.com/devkral/pam_email_extractor -# PAM_ENABLED=true -# Fallback email domain for email address generation (LOCAL_DOMAIN by default) -# PAM_EMAIL_DOMAIN=example.com -# Name of the pam service (pam "auth" section is evaluated) -# PAM_DEFAULT_SERVICE=rpam -# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default) -# PAM_CONTROLLED_SERVICE=rpam - -# Global OAuth settings (optional) : -# If you have only one strategy, you may want to enable this -# OAUTH_REDIRECT_AT_SIGN_IN=true - -# Optional CAS authentication (cf. omniauth-cas) : -# CAS_ENABLED=true -# CAS_URL=https://sso.myserver.com/ -# CAS_HOST=sso.myserver.com/ -# CAS_PORT=443 -# CAS_SSL=true -# CAS_VALIDATE_URL= -# CAS_CALLBACK_URL= -# CAS_LOGOUT_URL= -# CAS_LOGIN_URL= -# CAS_UID_FIELD='user' -# CAS_CA_PATH= -# CAS_DISABLE_SSL_VERIFICATION=false -# CAS_UID_KEY='user' -# CAS_NAME_KEY='name' -# CAS_EMAIL_KEY='email' -# CAS_NICKNAME_KEY='nickname' -# CAS_FIRST_NAME_KEY='firstname' -# CAS_LAST_NAME_KEY='lastname' -# CAS_LOCATION_KEY='location' -# CAS_IMAGE_KEY='image' -# CAS_PHONE_KEY='phone' - -# Optional SAML authentication (cf. omniauth-saml) -# SAML_ENABLED=true -# SAML_ACS_URL=http://localhost:3000/auth/auth/saml/callback -# SAML_ISSUER=https://example.com -# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO -# SAML_IDP_CERT= -# SAML_IDP_CERT_FINGERPRINT= -# SAML_NAME_IDENTIFIER_FORMAT= -# SAML_CERT= -# SAML_PRIVATE_KEY= -# SAML_SECURITY_WANT_ASSERTION_SIGNED=true -# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true -# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true -# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1" -# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" -# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.16.840.1.113730.3.1.241" -# SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME="urn:oid:2.5.4.42" -# SAML_ATTRIBUTES_STATEMENTS_LAST_NAME="urn:oid:2.5.4.4" -# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1" -# SAML_ATTRIBUTES_STATEMENTS_VERIFIED= -# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL= - - -# Custom settings -# --------------- -# Various ways to customize Mastodon's behavior -# --------------- - -# Maximum allowed character count -MAX_TOOT_CHARS=500 - -# Maximum allowed hashtags to follow in a feed column -# Note that setting this value higher may cause significant -# database load -MAX_FEED_HASHTAGS=4 - -# Maximum number of pinned posts -MAX_PINNED_TOOTS=5 - -# Maximum allowed bio characters -MAX_BIO_CHARS=500 - -# Maximim number of profile fields allowed -MAX_PROFILE_FIELDS=4 - -# Maximum allowed display name characters -MAX_DISPLAY_NAME_CHARS=30 - -# Maximum allowed poll options -MAX_POLL_OPTIONS=5 - -# Maximum allowed poll option characters -MAX_POLL_OPTION_CHARS=100 - -# Maximum number of emoji reactions per toot and user (minimum 1) -MAX_REACTIONS=1 - -# Maximum image and video/audio upload sizes -# Units are in bytes -# 1048576 bytes equals 1 megabyte -# MAX_IMAGE_SIZE=8388608 -# MAX_VIDEO_SIZE=41943040 - -# Maximum search results to display -# Only relevant when elasticsearch is installed -# MAX_SEARCH_RESULTS=20 - -# Maximum hashtags to display -# Customize the number of hashtags shown in 'Explore' -# MAX_TRENDING_TAGS=10 - -# Maximum custom emoji file sizes -# If undefined or smaller than MAX_EMOJI_SIZE, the value -# of MAX_EMOJI_SIZE will be used for MAX_REMOTE_EMOJI_SIZE -# Units are in bytes -# MAX_EMOJI_SIZE=262144 -# MAX_REMOTE_EMOJI_SIZE=262144 - -# Optional hCaptcha support -# HCAPTCHA_SECRET_KEY= -# HCAPTCHA_SITE_KEY= - -# IP and session retention -# ----------------------- -# Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml -# to be less than daily if you lower IP_RETENTION_PERIOD below two days (172800). -# ----------------------- -IP_RETENTION_PERIOD=31556952 -SESSION_RETENTION_PERIOD=31556952 diff --git a/.env.test b/.env.test deleted file mode 100644 index d2763e582a..0000000000 --- a/.env.test +++ /dev/null @@ -1,11 +0,0 @@ -# In test, compile the NodeJS code as if we are in production -NODE_ENV=production -# Federation -LOCAL_DOMAIN=cb6e6126.ngrok.io -LOCAL_HTTPS=true - -# Secret values required by ActiveRecord encryption feature -# Use `bin/rails db:encryption:init` to generate fresh secrets -ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=test_determinist_key_DO_NOT_USE_IN_PRODUCTION -ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=test_salt_DO_NOT_USE_IN_PRODUCTION -ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=test_primary_key_DO_NOT_USE_IN_PRODUCTION diff --git a/.env.vagrant b/.env.vagrant deleted file mode 100644 index 69c1bf1fb3..0000000000 --- a/.env.vagrant +++ /dev/null @@ -1,8 +0,0 @@ -VAGRANT=true -LOCAL_DOMAIN=mastodon.local -BIND=0.0.0.0 -DB_HOST=/var/run/postgresql/ - -ES_ENABLED=true -ES_HOST=localhost -ES_PORT=9200 \ No newline at end of file diff --git a/.foreman b/.foreman deleted file mode 100644 index 722b491f7f..0000000000 --- a/.foreman +++ /dev/null @@ -1 +0,0 @@ -procfile: Procfile.dev diff --git a/.github/actions/setup-javascript/action.yml b/.github/actions/setup-javascript/action.yml deleted file mode 100644 index 808adc7de6..0000000000 --- a/.github/actions/setup-javascript/action.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: 'Setup Javascript' -description: 'Setup a Javascript environment ready to run the Mastodon code' -inputs: - onlyProduction: - description: Only install production dependencies - default: 'false' - -runs: - using: 'composite' - steps: - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - - # The following is needed because we can not use `cache: true` for `setup-node`, as it does not support Corepack yet and mess up with the cache location if ran after Node is installed - - name: Enable corepack - shell: bash - run: corepack enable - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - shell: bash - run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT - - - uses: actions/cache@v4 - id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Install all yarn packages - shell: bash - run: yarn install --immutable - if: inputs.onlyProduction == 'false' - - - name: Install all production yarn packages - shell: bash - run: yarn workspaces focus --production - if: inputs.onlyProduction != 'false' diff --git a/.github/actions/setup-ruby/action.yml b/.github/actions/setup-ruby/action.yml deleted file mode 100644 index 3e232f134c..0000000000 --- a/.github/actions/setup-ruby/action.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: 'Setup RUby' -description: 'Setup a Ruby environment ready to run the Mastodon code' -inputs: - ruby-version: - description: The Ruby version to install - default: '.ruby-version' - additional-system-dependencies: - description: 'Additional packages to install' - -runs: - using: 'composite' - steps: - - name: Install system dependencies - shell: bash - run: | - sudo apt-get update - sudo apt-get install -y libicu-dev libidn11-dev libvips42 ${{ inputs.additional-system-dependencies }} - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ inputs.ruby-version }} - bundler-cache: true diff --git a/.github/codecov.yml b/.github/codecov.yml deleted file mode 100644 index 701ba3af8f..0000000000 --- a/.github/codecov.yml +++ /dev/null @@ -1,11 +0,0 @@ -comment: false # Do not leave PR comments -coverage: - status: - project: - default: - # GitHub status check is not blocking - informational: true - patch: - default: - # GitHub status check is not blocking - informational: true diff --git a/.github/renovate.json5 b/.github/renovate.json5 deleted file mode 100644 index 2cf7bec8ee..0000000000 --- a/.github/renovate.json5 +++ /dev/null @@ -1,158 +0,0 @@ -{ - $schema: 'https://docs.renovatebot.com/renovate-schema.json', - extends: [ - 'config:recommended', - 'customManagers:dockerfileVersions', - ':labels(dependencies)', - ':prConcurrentLimitNone', // Remove limit for open PRs at any time. - ':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour. - ], - minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it - // packageRules order is important, they are applied from top to bottom and are merged, - // meaning the most important ones must be at the bottom, for example grouping rules - // If we do not want a package to be grouped with others, we need to set its groupName - // to `null` after any other rule set it to something. - dependencyDashboardHeader: 'This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more. Before approving any upgrade: read the description and comments in the [`renovate.json5` file](https://github.com/mastodon/mastodon/blob/main/.github/renovate.json5).', - postUpdateOptions: ['yarnDedupeHighest'], - packageRules: [ - { - // Require Dependency Dashboard Approval for major version bumps of these node packages - matchManagers: ['npm'], - matchPackageNames: [ - 'tesseract.js', // Requires code changes - 'react-hotkeys', // Requires code changes - - // Requires Webpacker upgrade or replacement - '@svgr/webpack', - '@types/webpack', - 'babel-loader', - 'compression-webpack-plugin', - 'css-loader', - 'imports-loader', - 'mini-css-extract-plugin', - 'postcss-loader', - 'sass-loader', - 'terser-webpack-plugin', - 'webpack', - 'webpack-assets-manifest', - 'webpack-bundle-analyzer', - 'webpack-dev-server', - 'webpack-cli', - - // react-router: Requires manual upgrade - 'history', - 'react-router-dom', - ], - matchUpdateTypes: ['major'], - dependencyDashboardApproval: true, - }, - { - // Require Dependency Dashboard Approval for major version bumps of these Ruby packages - matchManagers: ['bundler'], - matchPackageNames: [ - 'rack', // Needs to be synced with Rails version - 'strong_migrations', // Requires manual upgrade - 'sidekiq', // Requires manual upgrade - 'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version - 'redis', // Requires manual upgrade and sync with Sidekiq version - ], - matchUpdateTypes: ['major'], - dependencyDashboardApproval: true, - }, - { - // Update GitHub Actions and Docker images weekly - matchManagers: ['github-actions', 'dockerfile', 'docker-compose'], - extends: ['schedule:weekly'], - }, - { - // Require Dependency Dashboard Approval for major & minor bumps for the ruby image, this needs to be synced with .ruby-version - matchManagers: ['dockerfile'], - matchPackageNames: ['moritzheiber/ruby-jemalloc'], - matchUpdateTypes: ['minor', 'major'], - dependencyDashboardApproval: true, - }, - { - // Require Dependency Dashboard Approval for major bumps for the node image, this needs to be synced with .nvmrc - matchManagers: ['dockerfile'], - matchPackageNames: ['node'], - matchUpdateTypes: ['major'], - dependencyDashboardApproval: true, - }, - { - // Require Dependency Dashboard Approval for major postgres bumps in the docker-compose file, as those break dev environments - matchManagers: ['docker-compose'], - matchPackageNames: ['postgres'], - matchUpdateTypes: ['major'], - dependencyDashboardApproval: true, - }, - { - // Update devDependencies every week, with one grouped PR - matchDepTypes: 'devDependencies', - matchUpdateTypes: ['patch', 'minor'], - groupName: 'devDependencies (non-major)', - extends: ['schedule:weekly'], - }, - { - // Group all eslint-related packages with `eslint` in the same PR - matchManagers: ['npm'], - matchPackageNames: ['eslint'], - matchPackagePrefixes: ['eslint-', '@typescript-eslint/'], - matchUpdateTypes: ['patch', 'minor'], - groupName: 'eslint (non-major)', - }, - { - // Group actions/*-artifact in the same PR - matchManagers: ['github-actions'], - matchPackageNames: [ - 'actions/download-artifact', - 'actions/upload-artifact', - ], - matchUpdateTypes: ['major'], - groupName: 'artifact actions (major)', - }, - { - // Update @types/* packages every week, with one grouped PR - matchPackagePrefixes: '@types/', - matchUpdateTypes: ['patch', 'minor'], - groupName: 'DefinitelyTyped types (non-major)', - extends: ['schedule:weekly'], - addLabels: ['typescript'], - }, - { - // We want those packages to always have their own PR - matchManagers: ['npm'], - matchPackageNames: [ - 'typescript', // Typescript has code-impacting changes in minor versions - ], - groupName: null, // We dont want them to belong to any group - }, - { - // Group all RuboCop packages with `rubocop` in the same PR - matchManagers: ['bundler'], - matchPackageNames: ['rubocop'], - matchPackagePrefixes: ['rubocop-'], - matchUpdateTypes: ['patch', 'minor'], - groupName: 'RuboCop (non-major)', - }, - { - // Group all RSpec packages with `rspec` in the same PR - matchManagers: ['bundler'], - matchPackageNames: ['rspec'], - matchPackagePrefixes: ['rspec-'], - matchUpdateTypes: ['patch', 'minor'], - groupName: 'RSpec (non-major)', - }, - { - // Group all opentelemetry-ruby packages in the same PR - matchManagers: ['bundler'], - matchPackagePrefixes: ['opentelemetry-'], - matchUpdateTypes: ['patch', 'minor'], - groupName: 'opentelemetry-ruby (non-major)', - }, - // Add labels depending on package manager - { matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] }, - { matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] }, - { matchManagers: ['docker-compose', 'dockerfile'], addLabels: ['docker'] }, - { matchManagers: ['github-actions'], addLabels: ['github_actions'] }, - ], -} diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 6601ef8c06..0000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,10 +0,0 @@ -daysUntilStale: 120 -daysUntilClose: 7 -exemptLabels: - - security -staleLabel: wontfix -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -only: pulls diff --git a/.github/workflows/build-container-image.yml b/.github/workflows/build-container-image.yml deleted file mode 100644 index dbb32af9bf..0000000000 --- a/.github/workflows/build-container-image.yml +++ /dev/null @@ -1,102 +0,0 @@ -on: - workflow_call: - inputs: - platforms: - required: true - type: string - cache: - type: boolean - default: true - use_native_arm64_builder: - type: boolean - push_to_images: - type: string - version_prerelease: - type: string - version_metadata: - type: string - flavor: - type: string - tags: - type: string - labels: - type: string - file_to_build: - type: string - -jobs: - build-image: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: docker/setup-qemu-action@v3 - if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder - - - uses: docker/setup-buildx-action@v3 - id: buildx - if: ${{ !(inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')) }} - - - name: Start a local Docker Builder - if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64') - run: | - docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234 - - - uses: docker/setup-buildx-action@v3 - id: buildx-native - if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64') - with: - driver: remote - endpoint: tcp://localhost:1234 - platforms: linux/amd64 - append: | - - endpoint: tcp://${{ vars.DOCKER_BUILDER_HETZNER_ARM64_01_HOST }}:13865 - platforms: linux/arm64 - name: mastodon-docker-builder-arm64-01 - driver-opts: - - servername=mastodon-docker-builder-arm64-01 - env: - BUILDER_NODE_1_AUTH_TLS_CACERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CACERT }} - BUILDER_NODE_1_AUTH_TLS_CERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CERT }} - BUILDER_NODE_1_AUTH_TLS_KEY: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_KEY }} - - - name: Log in to Docker Hub - if: contains(inputs.push_to_images, 'tootsuite') - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Log in to the GitHub Container registry - if: contains(inputs.push_to_images, 'ghcr.io') - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - uses: docker/metadata-action@v5 - id: meta - if: ${{ inputs.push_to_images != '' }} - with: - images: ${{ inputs.push_to_images }} - flavor: ${{ inputs.flavor }} - tags: ${{ inputs.tags }} - labels: ${{ inputs.labels }} - - - uses: docker/build-push-action@v5 - with: - context: . - file: ${{ inputs.file_to_build }} - build-args: | - MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }} - MASTODON_VERSION_METADATA=${{ inputs.version_metadata }} - platforms: ${{ inputs.platforms }} - provenance: false - builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }} - push: ${{ inputs.push_to_images != '' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: ${{ inputs.cache && 'type=gha' || '' }} - cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }} diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml deleted file mode 100644 index 18ec9e94e0..0000000000 --- a/.github/workflows/build-nightly.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Build nightly container image -on: - workflow_dispatch: - schedule: - - cron: '0 2 * * *' # run at 2 AM UTC - -permissions: - contents: read - packages: write - -jobs: - compute-suffix: - runs-on: ubuntu-latest - if: github.repository == 'TheEssem/mastodon' - steps: - - id: version_vars - env: - TZ: Etc/UTC - run: | - echo mastodon_version_prerelease=nightly.$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT - outputs: - prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }} - - build-image: - needs: compute-suffix - uses: ./.github/workflows/build-container-image.yml - with: - file_to_build: Dockerfile - platforms: linux/amd64,linux/arm64 - use_native_arm64_builder: false - cache: false - push_to_images: | - ghcr.io/${{ github.repository_owner }}/mastodon - version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }} - labels: | - org.opencontainers.image.description=Nightly build image used for testing purposes - flavor: | - latest=true - tags: | - type=raw,value=edge - type=raw,value=nightly - type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} - secrets: inherit - - build-image-streaming: - needs: compute-suffix - uses: ./.github/workflows/build-container-image.yml - with: - file_to_build: streaming/Dockerfile - platforms: linux/amd64,linux/arm64 - use_native_arm64_builder: false - cache: false - push_to_images: | - ghcr.io/${{ github.repository_owner }}/mastodon-streaming - version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }} - labels: | - org.opencontainers.image.description=Nightly build image used for testing purposes - flavor: | - latest=true - tags: | - type=raw,value=edge - type=raw,value=nightly - type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} - secrets: inherit diff --git a/.github/workflows/build-push-pr.yml b/.github/workflows/build-push-pr.yml deleted file mode 100644 index 4505151e1a..0000000000 --- a/.github/workflows/build-push-pr.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Build container image for PR -on: - pull_request: - types: [labeled, synchronize, reopened, ready_for_review, opened] - -permissions: - contents: read - packages: write - -jobs: - compute-suffix: - runs-on: ubuntu-latest - # This is only allowed to run if: - # - the PR branch is in the `mastodon/mastodon` repository - # - the PR is not a draft - # - the PR has the "build-image" label - if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !github.event.pull_request.draft && contains(github.event.pull_request.labels.*.name, 'build-image') }} - steps: - # Repository needs to be cloned so `git rev-parse` below works - - name: Clone repository - uses: actions/checkout@v4 - - id: version_vars - run: | - echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT - outputs: - metadata: ${{ steps.version_vars.outputs.mastodon_version_metadata }} - - build-image: - needs: compute-suffix - uses: ./.github/workflows/build-container-image.yml - with: - file_to_build: Dockerfile - platforms: linux/amd64,linux/arm64 - use_native_arm64_builder: false - push_to_images: | - ghcr.io/${{ github.repository_owner }}/mastodon - version_metadata: ${{ needs.compute-suffix.outputs.metadata }} - flavor: | - latest=auto - tags: | - type=ref,event=pr - secrets: inherit - - build-image-streaming: - needs: compute-suffix - uses: ./.github/workflows/build-container-image.yml - with: - file_to_build: streaming/Dockerfile - platforms: linux/amd64,linux/arm64 - use_native_arm64_builder: false - push_to_images: | - ghcr.io/${{ github.repository_owner }}/mastodon-streaming - version_metadata: ${{ needs.compute-suffix.outputs.metadata }} - flavor: | - latest=auto - tags: | - type=ref,event=pr - secrets: inherit diff --git a/.github/workflows/build-releases.yml b/.github/workflows/build-releases.yml deleted file mode 100644 index 8e0fe5dfa8..0000000000 --- a/.github/workflows/build-releases.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Build container release images -on: - push: - tags: - - '*' - -permissions: - contents: read - packages: write - -jobs: - build-image: - uses: ./.github/workflows/build-container-image.yml - with: - file_to_build: Dockerfile - platforms: linux/amd64,linux/arm64 - use_native_arm64_builder: false - push_to_images: | - ghcr.io/${{ github.repository_owner }}/mastodon - # Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages - cache: false - # Only tag with latest when ran against the latest stable branch - # This needs to be updated after each minor version release - flavor: | - latest=${{ startsWith(github.ref, 'refs/tags/v4.2.') }} - tags: | - type=pep440,pattern={{raw}} - type=pep440,pattern=v{{major}}.{{minor}} - secrets: inherit - - build-image-streaming: - if: startsWith(github.ref, 'refs/tags/v4.3.') - uses: ./.github/workflows/build-container-image.yml - with: - file_to_build: streaming/Dockerfile - platforms: linux/amd64,linux/arm64 - use_native_arm64_builder: false - push_to_images: | - ghcr.io/${{ github.repository_owner }}/mastodon-streaming - # Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages - cache: false - # Only tag with latest when ran against the latest stable branch - # This needs to be updated after each minor version release - flavor: | - latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }} - tags: | - type=pep440,pattern={{raw}} - type=pep440,pattern=v{{major}}.{{minor}} - secrets: inherit diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml deleted file mode 100644 index e9f1862f5d..0000000000 --- a/.github/workflows/build-security.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Build security nightly container image -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -jobs: - compute-suffix: - runs-on: ubuntu-latest - steps: - - id: version_vars - env: - TZ: Etc/UTC - run: | - echo mastodon_version_prerelease=nightly.$(date --date='next day' +'%Y-%m-%d')-security>> $GITHUB_OUTPUT - outputs: - prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }} - - build-image: - needs: compute-suffix - uses: ./.github/workflows/build-container-image.yml - with: - file_to_build: Dockerfile - platforms: linux/amd64,linux/arm64 - use_native_arm64_builder: false - cache: false - push_to_images: | - ghcr.io/${{ github.repository_owner }}/mastodon - version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }} - labels: | - org.opencontainers.image.description=Nightly build image used for testing purposes - flavor: | - latest=true - tags: | - type=raw,value=edge - type=raw,value=nightly - type=raw,value=${{ needs.compute-suffix.outputs.prerelease }} - secrets: inherit - - build-image-streaming: - needs: compute-suffix - uses: ./.github/workflows/build-container-image.yml - with: - file_to_build: streaming/Dockerfile - platforms: linux/amd64,linux/arm64 - use_native_arm64_builder: false - cache: false - push_to_images: | - ghcr.io/${{ github.repository_owner }}/mastodon-streaming - version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }} - labels: | - org.opencontainers.image.description=Nightly build image used for testing purposes - flavor: | - latest=true - tags: | - type=raw,value=edge - type=raw,value=nightly - type=raw,value=${{ needs.compute-suffix.outputs.prerelease }} - secrets: inherit diff --git a/.github/workflows/bundler-audit.yml b/.github/workflows/bundler-audit.yml deleted file mode 100644 index 2341d6e67f..0000000000 --- a/.github/workflows/bundler-audit.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Bundler Audit -on: - merge_group: - push: - branches: - - 'main' - - 'stable-*' - paths: - - 'Gemfile*' - - '.ruby-version' - - '.github/workflows/bundler-audit.yml' - - pull_request: - paths: - - 'Gemfile*' - - '.ruby-version' - - '.github/workflows/bundler-audit.yml' - - schedule: - - cron: '0 5 * * 1' - -jobs: - security: - runs-on: ubuntu-latest - - env: - BUNDLE_ONLY: development - - steps: - - name: Clone repository - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - name: Run bundler-audit - run: bundle exec bundler-audit check --update diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml deleted file mode 100644 index 5a1c051966..0000000000 --- a/.github/workflows/check-i18n.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Check i18n - -on: - push: - branches: - - 'main' - - 'stable-*' - pull_request: - branches: - - 'main' - - 'stable-*' - -env: - RAILS_ENV: test - -permissions: - contents: read - -jobs: - check-i18n: - runs-on: ubuntu-22.04 - - steps: - - uses: actions/checkout@v4 - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript - - - name: Check for missing strings in English JSON - run: | - yarn i18n:extract --throws - git diff --exit-code - - - name: Check locale file normalization - run: bundle exec i18n-tasks check-normalized - - - name: Check for unused strings - run: bundle exec i18n-tasks unused - - - name: Check for missing strings in English YML - run: | - bundle exec i18n-tasks add-missing -l en - git diff --exit-code - - - name: Check for wrong string interpolations - run: bundle exec i18n-tasks check-consistent-interpolations - - - name: Check that all required locale files exist - run: bundle exec rake repo:check_locales_files diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 8690e9ed6d..0000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: 'CodeQL' - -on: - merge_group: - push: - branches: - - 'main' - - 'stable-*' - pull_request: - branches: - - 'main' - - 'stable-*' - schedule: - - cron: '22 6 * * 1' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: ['javascript', 'ruby'] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: '/language:${{matrix.language}}' diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml deleted file mode 100644 index 1212e66296..0000000000 --- a/.github/workflows/crowdin-download.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Crowdin / Download translations -on: - schedule: - - cron: '17 4 * * *' # Every day - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - download-translations: - runs-on: ubuntu-latest - if: github.repository == 'glitch-soc/mastodon' - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Increase Git http.postBuffer - # This is needed due to a bug in Ubuntu's cURL version? - # See https://github.com/orgs/community/discussions/55820 - run: | - git config --global http.version HTTP/1.1 - git config --global http.postBuffer 157286400 - - # Download the translation files from Crowdin - - name: crowdin action - uses: crowdin/github-action@v1 - with: - config: crowdin-glitch.yml - upload_sources: false - upload_translations: false - download_translations: true - crowdin_branch_name: main - push_translations: false - create_pull_request: false - env: - CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - - # As the files are extracted from a Docker container, they belong to root:root - # We need to fix this before the next steps - - name: Fix file permissions - run: sudo chown -R runner:docker . - - # This is needed to run the normalize step - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - - - name: Run i18n normalize task - run: bundle exec i18n-tasks normalize - - # Create or update the pull request - - name: Create Pull Request - uses: peter-evans/create-pull-request@v6.0.5 - with: - commit-message: 'New Crowdin translations' - title: 'New Crowdin Translations (automated)' - author: 'GitHub Actions ' - body: | - New Crowdin translations, automated with GitHub Actions - - See `.github/workflows/crowdin-download.yml` - - This PR will be updated every day with new translations. - - Due to a limitation in GitHub Actions, checks are not running on this PR without manual action. - If you want to run the checks, then close and re-open it. - branch: i18n/crowdin/translations - base: main - labels: i18n diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml deleted file mode 100644 index 18559a62b6..0000000000 --- a/.github/workflows/crowdin-upload.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Crowdin / Upload translations - -on: - merge_group: - push: - branches: - - 'main' - - 'stable-*' - paths: - - crowdin-glitch.yml - - app/javascript/flavours/glitch/locales/en.json - - config/locales-glitch/en.yml - - config/locales-glitch/simple_form.en.yml - - config/locales-glitch/activerecord.en.yml - - config/locales-glitch/devise.en.yml - - config/locales-glitch/doorkeeper.en.yml - - .github/workflows/crowdin-upload.yml - -jobs: - upload-translations: - runs-on: ubuntu-latest - if: github.repository == 'mastodon/mastodon' - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: crowdin action - uses: crowdin/github-action@v1 - with: - config: crowdin-glitch.yml - upload_sources: true - upload_translations: false - download_translations: false - crowdin_branch_name: main - - env: - CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml deleted file mode 100644 index c10f350a02..0000000000 --- a/.github/workflows/format-check.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Check formatting -on: - merge_group: - push: - branches: - - 'main' - - 'stable-*' - pull_request: - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Clone repository - uses: actions/checkout@v4 - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript - - - name: Check formatting with Prettier - run: yarn format:check diff --git a/.github/workflows/haml-lint-problem-matcher.json b/.github/workflows/haml-lint-problem-matcher.json deleted file mode 100644 index 3523ea2951..0000000000 --- a/.github/workflows/haml-lint-problem-matcher.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "problemMatcher": [ - { - "owner": "haml-lint", - "severity": "warning", - "pattern": [ - { - "regexp": "^(.*):(\\d+)\\s\\[W]\\s(.*):\\s(.*)$", - "file": 1, - "line": 2, - "code": 3, - "message": 4 - } - ] - } - ] -} diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml deleted file mode 100644 index 95fcd56942..0000000000 --- a/.github/workflows/lint-css.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: CSS Linting -on: - merge_group: - push: - branches: - - 'main' - - 'stable-*' - paths: - - 'package.json' - - 'yarn.lock' - - '.nvmrc' - - '.prettier*' - - 'stylelint.config.js' - - '**/*.css' - - '**/*.scss' - - '.github/workflows/lint-css.yml' - - '.github/stylelint-matcher.json' - - pull_request: - paths: - - 'package.json' - - 'yarn.lock' - - '.nvmrc' - - '.prettier*' - - 'stylelint.config.js' - - '**/*.css' - - '**/*.scss' - - '.github/workflows/lint-css.yml' - - '.github/stylelint-matcher.json' - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Clone repository - uses: actions/checkout@v4 - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript - - - name: Stylelint - run: yarn lint:css -f github diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml deleted file mode 100644 index a1a9e99c90..0000000000 --- a/.github/workflows/lint-haml.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Haml Linting -on: - merge_group: - push: - branches: - - 'main' - - 'stable-*' - paths: - - '.github/workflows/haml-lint-problem-matcher.json' - - '.github/workflows/lint-haml.yml' - - '.haml-lint*.yml' - - '.rubocop*.yml' - - '.ruby-version' - - '**/*.haml' - - 'Gemfile*' - - pull_request: - paths: - - '.github/workflows/haml-lint-problem-matcher.json' - - '.github/workflows/lint-haml.yml' - - '.haml-lint*.yml' - - '.rubocop*.yml' - - '.ruby-version' - - '**/*.haml' - - 'Gemfile*' - -jobs: - lint: - runs-on: ubuntu-latest - - env: - BUNDLE_ONLY: development - - steps: - - name: Clone repository - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - name: Run haml-lint - run: | - echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json" - bundle exec haml-lint --reporter github diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml deleted file mode 100644 index 7d31a5e20e..0000000000 --- a/.github/workflows/lint-js.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: JavaScript Linting -on: - merge_group: - push: - branches: - - 'main' - - 'stable-*' - paths: - - 'package.json' - - 'yarn.lock' - - 'tsconfig.json' - - '.nvmrc' - - '.prettier*' - - '.eslint*' - - '**/*.js' - - '**/*.jsx' - - '**/*.ts' - - '**/*.tsx' - - '.github/workflows/lint-js.yml' - - pull_request: - paths: - - 'package.json' - - 'yarn.lock' - - 'tsconfig.json' - - '.nvmrc' - - '.prettier*' - - '.eslint*' - - '**/*.js' - - '**/*.jsx' - - '**/*.ts' - - '**/*.tsx' - - '.github/workflows/lint-js.yml' - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Clone repository - uses: actions/checkout@v4 - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript - - - name: ESLint - run: yarn lint:js --max-warnings 0 - - - name: Typecheck - run: yarn typecheck diff --git a/.github/workflows/lint-ruby.yml b/.github/workflows/lint-ruby.yml deleted file mode 100644 index 277e456146..0000000000 --- a/.github/workflows/lint-ruby.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Ruby Linting -on: - merge_group: - push: - branches: - - 'main' - - 'stable-*' - paths: - - 'Gemfile*' - - '.rubocop*.yml' - - '.ruby-version' - - 'config/brakeman.ignore' - - '**/*.rb' - - '**/*.rake' - - '.github/workflows/lint-ruby.yml' - - pull_request: - paths: - - 'Gemfile*' - - '.rubocop*.yml' - - '.ruby-version' - - 'config/brakeman.ignore' - - '**/*.rb' - - '**/*.rake' - - '.github/workflows/lint-ruby.yml' - -jobs: - lint: - runs-on: ubuntu-latest - - env: - BUNDLE_ONLY: development - - steps: - - name: Clone repository - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - - name: Set-up RuboCop Problem Matcher - uses: r7kamura/rubocop-problem-matchers-action@v1 - - - name: Run rubocop - run: bin/rubocop - - - name: Run brakeman - if: always() # Run both checks, even if the first failed - run: bin/brakeman diff --git a/.github/workflows/rebase-needed.yml b/.github/workflows/rebase-needed.yml deleted file mode 100644 index 8784397a8f..0000000000 --- a/.github/workflows/rebase-needed.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: PR Needs Rebase - -on: - schedule: - - cron: '0 * * * *' - -permissions: - pull-requests: write - -jobs: - label-rebase-needed: - runs-on: ubuntu-latest - - concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - steps: - - name: Check for merge conflicts - uses: eps1lon/actions-label-merge-conflict@v3 - with: - dirtyLabel: 'rebase needed :construction:' - repoToken: '${{ secrets.GITHUB_TOKEN }}' - commentOnClean: This pull request has resolved merge conflicts and is ready for review. - commentOnDirty: This pull request has merge conflicts that must be resolved before it can be merged. - retryMax: 30 - continueOnMissingPermissions: false diff --git a/.github/workflows/test-image-build.yml b/.github/workflows/test-image-build.yml deleted file mode 100644 index 980e071897..0000000000 --- a/.github/workflows/test-image-build.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Test container image build -on: - pull_request: - paths: - - .github/workflows/build-nightly.yml - - .github/workflows/build-push-pr.yml - - .github/workflows/build-releases.yml - - .github/workflows/test-image-build.yml - - Dockerfile - - streaming/Dockerfile -permissions: - contents: read - -jobs: - build-image: - concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - uses: ./.github/workflows/build-container-image.yml - with: - file_to_build: Dockerfile - platforms: linux/amd64 # Testing only on native platform so it is performant - cache: true - - build-image-streaming: - concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-streaming - cancel-in-progress: true - - uses: ./.github/workflows/build-container-image.yml - with: - file_to_build: streaming/Dockerfile - platforms: linux/amd64 # Testing only on native platform so it is performant - cache: true diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml deleted file mode 100644 index e9e43ac9e8..0000000000 --- a/.github/workflows/test-js.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: JavaScript Testing -on: - merge_group: - push: - branches: - - 'main' - - 'stable-*' - paths: - - 'package.json' - - 'yarn.lock' - - '.nvmrc' - - '**/*.js' - - '**/*.jsx' - - '**/*.ts' - - '**/*.tsx' - - '**/*.snap' - - '.github/workflows/test-js.yml' - - pull_request: - paths: - - 'package.json' - - 'yarn.lock' - - '.nvmrc' - - '**/*.js' - - '**/*.jsx' - - '**/*.ts' - - '**/*.tsx' - - '**/*.snap' - - '.github/workflows/test-js.yml' - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Clone repository - uses: actions/checkout@v4 - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript - - - name: JavaScript testing - run: yarn jest --reporters github-actions summary diff --git a/.github/workflows/test-migrations.yml b/.github/workflows/test-migrations.yml deleted file mode 100644 index 6a0e67c58e..0000000000 --- a/.github/workflows/test-migrations.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Historical data migration test - -on: - merge_group: - push: - branches: - - 'main' - - 'stable-*' - paths: - - 'Gemfile*' - - '.ruby-version' - - '**/*.rb' - - '.github/workflows/test-migrations.yml' - - 'lib/tasks/tests.rake' - - pull_request: - paths: - - 'Gemfile*' - - '.ruby-version' - - '**/*.rb' - - '.github/workflows/test-migrations.yml' - - 'lib/tasks/tests.rake' - -jobs: - test: - runs-on: ubuntu-latest - - strategy: - fail-fast: false - - matrix: - postgres: - - 14-alpine - - 15-alpine - - services: - postgres: - image: postgres:${{ matrix.postgres}} - env: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - options: >- - --health-cmd pg_isready - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 6379:6379 - - env: - DB_HOST: localhost - DB_USER: postgres - DB_PASS: postgres - DISABLE_SIMPLECOV: true - RAILS_ENV: test - BUNDLE_CLEAN: true - BUNDLE_FROZEN: true - BUNDLE_WITHOUT: 'development:production' - BUNDLE_JOBS: 3 - BUNDLE_RETRY: 3 - - steps: - - uses: actions/checkout@v4 - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - - - name: Test "one step migration" flow - run: | - bin/rails db:drop - bin/rails db:create - bin/rails tests:migrations:prepare_database - bin/rails db:migrate - bin/rails tests:migrations:check_database - - - name: Test "two step migration" flow - run: | - bin/rails db:drop - bin/rails db:create - SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails tests:migrations:prepare_database - SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:migrate - bin/rails db:migrate - bin/rails tests:migrations:check_database diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml deleted file mode 100644 index fcfeed5fba..0000000000 --- a/.github/workflows/test-ruby.yml +++ /dev/null @@ -1,444 +0,0 @@ -name: Ruby Testing - -on: - merge_group: - push: - branches: - - 'main' - - 'stable-*' - pull_request: - -env: - BUNDLE_CLEAN: true - BUNDLE_FROZEN: true - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - fail-fast: true - matrix: - mode: - - production - - test - env: - RAILS_ENV: ${{ matrix.mode }} - BUNDLE_WITH: ${{ matrix.mode }} - SECRET_KEY_BASE_DUMMY: 1 - - steps: - - uses: actions/checkout@v4 - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript - with: - onlyProduction: 'true' - - - name: Precompile assets - # Previously had set this, but it's not supported - # export NODE_OPTIONS=--openssl-legacy-provider - run: |- - ./bin/rails assets:precompile - - - name: Archive asset artifacts - run: | - tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* - - - uses: actions/upload-artifact@v4 - if: matrix.mode == 'test' - with: - path: |- - ./artifacts.tar.gz - name: ${{ github.sha }} - retention-days: 0 - - test: - runs-on: ubuntu-latest - - needs: - - build - - services: - postgres: - image: postgres:14-alpine - env: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - options: >- - --health-cmd pg_isready - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 6379:6379 - - env: - DB_HOST: localhost - DB_USER: postgres - DB_PASS: postgres - DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }} - RAILS_ENV: test - ALLOW_NOPAM: true - PAM_ENABLED: true - PAM_DEFAULT_SERVICE: pam_test - PAM_CONTROLLED_SERVICE: pam_test_controlled - OIDC_ENABLED: true - OIDC_SCOPE: read - SAML_ENABLED: true - CAS_ENABLED: true - BUNDLE_WITH: 'pam_authentication test' - GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} - - strategy: - fail-fast: false - matrix: - ruby-version: - - '3.1' - - '3.2' - - '.ruby-version' - steps: - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - path: './' - name: ${{ github.sha }} - - - name: Expand archived asset artifacts - run: | - tar xvzf artifacts.tar.gz - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - with: - ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg libpam-dev - - - name: Load database schema - run: | - bin/rails db:setup - bin/flatware fan bin/rails db:test:prepare - - - run: bin/flatware rspec -r ./spec/flatware_helper.rb - - - name: Upload coverage reports to Codecov - if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v4 - with: - files: coverage/lcov/*.lcov - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - test-libvips: - name: Libvips tests - runs-on: ubuntu-24.04 - - needs: - - build - - services: - postgres: - image: postgres:14-alpine - env: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - options: >- - --health-cmd pg_isready - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 6379:6379 - - env: - DB_HOST: localhost - DB_USER: postgres - DB_PASS: postgres - DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }} - RAILS_ENV: test - ALLOW_NOPAM: true - PAM_ENABLED: true - PAM_DEFAULT_SERVICE: pam_test - PAM_CONTROLLED_SERVICE: pam_test_controlled - OIDC_ENABLED: true - OIDC_SCOPE: read - SAML_ENABLED: true - CAS_ENABLED: true - BUNDLE_WITH: 'pam_authentication test' - GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} - MASTODON_USE_LIBVIPS: true - - strategy: - fail-fast: false - matrix: - ruby-version: - - '3.1' - - '3.2' - - '.ruby-version' - steps: - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - path: './' - name: ${{ github.sha }} - - - name: Expand archived asset artifacts - run: | - tar xvzf artifacts.tar.gz - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - with: - ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg libpam-dev libyaml-dev - - - name: Load database schema - run: './bin/rails db:create db:schema:load db:seed' - - - run: bin/rspec --tag attachment_processing - - - name: Upload coverage reports to Codecov - if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v4 - with: - files: coverage/lcov/mastodon.lcov - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - test-e2e: - name: End to End testing - runs-on: ubuntu-latest - - needs: - - build - - services: - postgres: - image: postgres:14-alpine - env: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - options: >- - --health-cmd pg_isready - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 6379:6379 - - env: - DB_HOST: localhost - DB_USER: postgres - DB_PASS: postgres - DISABLE_SIMPLECOV: true - RAILS_ENV: test - BUNDLE_WITH: test - LOCAL_DOMAIN: localhost:3000 - LOCAL_HTTPS: false - - strategy: - fail-fast: false - matrix: - ruby-version: - - '3.1' - - '3.2' - - '.ruby-version' - - steps: - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - path: './' - name: ${{ github.sha }} - - - name: Expand archived asset artifacts - run: | - tar xvzf artifacts.tar.gz - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - with: - ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript - - - name: Load database schema - run: './bin/rails db:create db:schema:load db:seed' - - - run: bin/rspec spec/system --tag streaming --tag js - - - name: Archive logs - uses: actions/upload-artifact@v4 - if: failure() - with: - name: e2e-logs-${{ matrix.ruby-version }} - path: log/ - - - name: Archive test screenshots - uses: actions/upload-artifact@v4 - if: failure() - with: - name: e2e-screenshots - path: tmp/capybara/ - - test-search: - name: Elastic Search integration testing - runs-on: ubuntu-latest - - needs: - - build - - services: - postgres: - image: postgres:14-alpine - env: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - options: >- - --health-cmd pg_isready - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 6379:6379 - - elasticsearch: - image: ${{ contains(matrix.search-image, 'elasticsearch') && matrix.search-image || '' }} - env: - discovery.type: single-node - xpack.security.enabled: false - options: >- - --health-cmd "curl http://localhost:9200/_cluster/health" - --health-interval 2s - --health-timeout 3s - --health-retries 50 - ports: - - 9200:9200 - - opensearch: - image: ${{ contains(matrix.search-image, 'opensearch') && matrix.search-image || '' }} - env: - discovery.type: single-node - DISABLE_INSTALL_DEMO_CONFIG: true - DISABLE_SECURITY_PLUGIN: true - options: >- - --health-cmd "curl http://localhost:9200/_cluster/health" - --health-interval 2s - --health-timeout 3s - --health-retries 50 - ports: - - 9200:9200 - - env: - DB_HOST: localhost - DB_USER: postgres - DB_PASS: postgres - DISABLE_SIMPLECOV: true - RAILS_ENV: test - BUNDLE_WITH: test - ES_ENABLED: true - ES_HOST: localhost - ES_PORT: 9200 - - strategy: - fail-fast: false - matrix: - ruby-version: - - '3.1' - - '3.2' - - '.ruby-version' - search-image: - - docker.elastic.co/elasticsearch/elasticsearch:7.17.13 - include: - - ruby-version: '.ruby-version' - search-image: docker.elastic.co/elasticsearch/elasticsearch:8.10.2 - - ruby-version: '.ruby-version' - search-image: opensearchproject/opensearch:2 - - steps: - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - path: './' - name: ${{ github.sha }} - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - with: - ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript - - - name: Load database schema - run: './bin/rails db:create db:schema:load db:seed' - - - run: bin/rspec --tag search - - - name: Archive logs - uses: actions/upload-artifact@v4 - if: failure() - with: - name: test-search-logs-${{ matrix.ruby-version }} - path: log/ - - - name: Archive test screenshots - uses: actions/upload-artifact@v4 - if: failure() - with: - name: test-search-screenshots - path: tmp/capybara/ diff --git a/.haml-lint.yml b/.haml-lint.yml deleted file mode 100644 index 74d243a3ad..0000000000 --- a/.haml-lint.yml +++ /dev/null @@ -1,15 +0,0 @@ -exclude: - - 'vendor/**/*' - -require: - - ./lib/linter/haml_middle_dot.rb - -linters: - AltText: - enabled: true - MiddleDot: - enabled: true - LineLength: - max: 300 - ViewLength: - max: 200 # Override default value of 100 inherited from rubocop diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 3723623171..0000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -yarn lint-staged diff --git a/.rubocop.yml b/.rubocop.yml deleted file mode 100644 index 965f56f3e7..0000000000 --- a/.rubocop.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -AllCops: - CacheRootDirectory: tmp - DisplayStyleGuide: true - Exclude: - - Vagrantfile - - config/initializers/json_ld* - - lib/mastodon/migration_helpers.rb - ExtraDetails: true - NewCops: enable - TargetRubyVersion: 3.1 # Oldest supported ruby version - -inherit_from: - - .rubocop/layout.yml - - .rubocop/metrics.yml - - .rubocop/naming.yml - - .rubocop/rails.yml - - .rubocop/rspec_rails.yml - - .rubocop/rspec.yml - - .rubocop/style.yml - - .rubocop/custom.yml - - .rubocop_todo.yml - - .rubocop/strict.yml - -inherit_mode: - merge: - - Exclude - -require: - - rubocop-rails - - rubocop-rspec - - rubocop-rspec_rails - - rubocop-performance - - rubocop-capybara diff --git a/.rubocop/custom.yml b/.rubocop/custom.yml deleted file mode 100644 index 63035837f8..0000000000 --- a/.rubocop/custom.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -require: - - ../lib/linter/rubocop_middle_dot - -Style/MiddleDot: - Enabled: true diff --git a/.rubocop/layout.yml b/.rubocop/layout.yml deleted file mode 100644 index 487879ca2c..0000000000 --- a/.rubocop/layout.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -Layout/FirstHashElementIndentation: - EnforcedStyle: consistent - -Layout/LineLength: - Max: 300 # Default of 120 causes a duplicate entry in generated todo file diff --git a/.rubocop/metrics.yml b/.rubocop/metrics.yml deleted file mode 100644 index 89532af42a..0000000000 --- a/.rubocop/metrics.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -Metrics/AbcSize: - Exclude: - - lib/mastodon/cli/*.rb - -Metrics/BlockLength: - Enabled: false - -Metrics/ClassLength: - Enabled: false - -Metrics/CyclomaticComplexity: - Exclude: - - lib/mastodon/cli/*.rb - -Metrics/MethodLength: - Enabled: false - -Metrics/ModuleLength: - Enabled: false - -Metrics/ParameterLists: - CountKeywordArgs: false diff --git a/.rubocop/naming.yml b/.rubocop/naming.yml deleted file mode 100644 index da6ad4ac57..0000000000 --- a/.rubocop/naming.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -Naming/BlockForwarding: - EnforcedStyle: explicit diff --git a/.rubocop/rails.yml b/.rubocop/rails.yml deleted file mode 100644 index ae31c1f266..0000000000 --- a/.rubocop/rails.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -Rails/BulkChangeTable: - Enabled: false # Conflicts with strong_migrations features - -Rails/FilePath: - EnforcedStyle: arguments - -Rails/HttpStatus: - EnforcedStyle: numeric - -Rails/NegateInclude: - Enabled: false - -Rails/RakeEnvironment: - Exclude: # Tasks are doing local work which do not need full env loaded - - lib/tasks/auto_annotate_models.rake - - lib/tasks/emojis.rake - - lib/tasks/mastodon.rake - - lib/tasks/repo.rake - - lib/tasks/statistics.rake - -Rails/SkipsModelValidations: - Enabled: false diff --git a/.rubocop/rspec.yml b/.rubocop/rspec.yml deleted file mode 100644 index d2d2f8325d..0000000000 --- a/.rubocop/rspec.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- -RSpec/ExampleLength: - CountAsOne: ['array', 'heredoc', 'method_call'] - Max: 20 # Override default of 5 - -RSpec/MultipleExpectations: - Max: 10 # Overrides default of 1 - -RSpec/MultipleMemoizedHelpers: - Max: 20 # Overrides default of 5 - -RSpec/NamedSubject: - EnforcedStyle: named_only - -RSpec/NestedGroups: - Max: 10 # Overrides default of 3 - -RSpec/NotToNot: - EnforcedStyle: to_not - -RSpec/SpecFilePathFormat: - CustomTransform: - ActivityPub: activitypub - DeepL: deepl - FetchOEmbedService: fetch_oembed_service - OEmbedController: oembed_controller - OStatus: ostatus diff --git a/.rubocop/rspec_rails.yml b/.rubocop/rspec_rails.yml deleted file mode 100644 index 993a5689ad..0000000000 --- a/.rubocop/rspec_rails.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -RSpecRails/HttpStatus: - EnforcedStyle: numeric diff --git a/.rubocop/strict.yml b/.rubocop/strict.yml deleted file mode 100644 index 2222c6d8b9..0000000000 --- a/.rubocop/strict.yml +++ /dev/null @@ -1,19 +0,0 @@ -Lint/Debugger: # Remove any `binding.pry` - Enabled: true - Exclude: [] - -RSpec/Focus: # Require full spec run on CI - Enabled: true - Exclude: [] - -Rails/Output: # Remove any `puts` debugging - Enabled: true - Exclude: [] - -Rails/FindEach: # Using `each` could impact performance, use `find_each` - Enabled: true - Exclude: [] - -Rails/UniqBeforePluck: # Require `uniq.pluck` and not `pluck.uniq` - Enabled: true - Exclude: [] diff --git a/.rubocop/style.yml b/.rubocop/style.yml deleted file mode 100644 index 03e35a70ac..0000000000 --- a/.rubocop/style.yml +++ /dev/null @@ -1,47 +0,0 @@ ---- -Style/ClassAndModuleChildren: - Enabled: false - -Style/Documentation: - Enabled: false - -Style/FormatStringToken: - AllowedMethods: - - redirect_with_vary # Route redirects are not token-formatted - inherit_mode: - merge: - - AllowedMethods - -Style/HashAsLastArrayItem: - Enabled: false - -Style/HashSyntax: - EnforcedShorthandSyntax: either - EnforcedStyle: ruby19_no_mixed_keys - -Style/NumericLiterals: - AllowedPatterns: - - \d{4}_\d{2}_\d{2}_\d{6} - -Style/PercentLiteralDelimiters: - PreferredDelimiters: - '%i': () - '%w': () - -Style/RedundantBegin: - Enabled: false - -Style/RedundantFetchBlock: - Enabled: false - -Style/RescueStandardError: - EnforcedStyle: implicit - -Style/SymbolArray: - Enabled: false - -Style/TrailingCommaInArrayLiteral: - EnforcedStyleForMultiline: comma - -Style/TrailingCommaInHashLiteral: - EnforcedStyleForMultiline: comma diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml deleted file mode 100644 index 2549202410..0000000000 --- a/.rubocop_todo.yml +++ /dev/null @@ -1,117 +0,0 @@ -# This configuration was generated by -# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.65.0. -# The point is for the user to remove these configuration records -# one by one as the offenses are removed from the code base. -# Note that changes in the inspected code, or installation of new -# versions of RuboCop, may require this file to be generated again. - -Lint/NonLocalExitFromIterator: - Exclude: - - 'app/helpers/jsonld_helper.rb' - -# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. -Metrics/AbcSize: - Max: 90 - -# Configuration parameters: CountBlocks, CountModifierForms, Max. -Metrics/BlockNesting: - Exclude: - - 'lib/tasks/mastodon.rake' - -# Configuration parameters: AllowedMethods, AllowedPatterns. -Metrics/CyclomaticComplexity: - Max: 25 - -# Configuration parameters: AllowedMethods, AllowedPatterns. -Metrics/PerceivedComplexity: - Max: 27 - -Rails/OutputSafety: - Exclude: - - 'config/initializers/simple_form.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedVars. -Style/FetchEnvVar: - Exclude: - - 'app/lib/redis_configuration.rb' - - 'app/lib/translation_service.rb' - - 'config/environments/production.rb' - - 'config/initializers/2_limited_federation_mode.rb' - - 'config/initializers/3_omniauth.rb' - - 'config/initializers/blacklists.rb' - - 'config/initializers/cache_buster.rb' - - 'config/initializers/devise.rb' - - 'config/initializers/paperclip.rb' - - 'config/initializers/vapid.rb' - - 'lib/mastodon/redis_config.rb' - - 'lib/tasks/repo.rake' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. -# SupportedStyles: annotated, template, unannotated -# AllowedMethods: redirect -Style/FormatStringToken: - Exclude: - - 'config/initializers/devise.rb' - - 'lib/paperclip/color_extractor.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. -Style/GuardClause: - Enabled: false - -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/HashTransformValues: - Exclude: - - 'app/serializers/rest/web_push_subscription_serializer.rb' - - 'app/services/import_service.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/MapToHash: - Exclude: - - 'app/models/status.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: literals, strict -Style/MutableConstant: - Exclude: - - 'app/models/tag.rb' - - 'app/services/delete_account_service.rb' - - 'lib/mastodon/migration_warning.rb' - -# Configuration parameters: AllowedMethods. -# AllowedMethods: respond_to_missing? -Style/OptionalBooleanParameter: - Exclude: - - 'app/helpers/jsonld_helper.rb' - - 'app/lib/admin/system_check/message.rb' - - 'app/lib/request.rb' - - 'app/lib/webfinger.rb' - - 'app/services/block_domain_service.rb' - - 'app/services/fetch_resource_service.rb' - - 'app/workers/domain_block_worker.rb' - - 'app/workers/unfollow_follow_worker.rb' - - 'lib/mastodon/redis_config.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: short, verbose -Style/PreferredHashMethods: - Exclude: - - 'config/initializers/paperclip.rb' - -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantConstantBase: - Exclude: - - 'config/environments/production.rb' - - 'config/initializers/sidekiq.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: WordRegex. -# SupportedStyles: percent, brackets -Style/WordArray: - EnforcedStyle: percent - MinSize: 3 diff --git a/Aptfile b/Aptfile deleted file mode 100644 index 5e033f1365..0000000000 --- a/Aptfile +++ /dev/null @@ -1,5 +0,0 @@ -ffmpeg -libopenblas0-pthread -libpq-dev -libxdamage1 -libxfixes3 diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 7c3d96ba4a..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,1122 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -## [4.2.10] - 2024-07-04 - -### Security - -- Fix incorrect permission checking on multiple API endpoints ([GHSA-58x8-3qxw-6hm7](https://github.com/mastodon/mastodon/security/advisories/GHSA-58x8-3qxw-6hm7)) -- Fix incorrect authorship checking when processing some activities (CVE-2024-37903, [GHSA-xjvf-fm67-4qc3](https://github.com/mastodon/mastodon/security/advisories/GHSA-xjvf-fm67-4qc3)) -- Fix ongoing streaming sessions not being invalidated when application tokens get revoked ([GHSA-vp5r-5pgw-jwqx](https://github.com/mastodon/mastodon/security/advisories/GHSA-vp5r-5pgw-jwqx)) -- Update dependencies - -### Added - -- Add yarn version specification to avoid confusion with Yarn 3 and Yarn 4 - -### Changed - -- Change preview cards generation to skip unusually long URLs ([oneiros](https://github.com/mastodon/mastodon/pull/30854)) -- Change search modifiers to be case-insensitive ([Gargron](https://github.com/mastodon/mastodon/pull/30865)) -- Change `STATSD_ADDR` handling to emit a warning rather than crashing if the address is unreachable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30691)) -- Change PWA start URL from `/home` to `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27377)) - -### Removed - -- Removed dependency on `posix-spawn` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18559)) - -### Fixed - -- Fix scheduled statuses scheduled in less than 5 minutes being immediately published ([danielmbrasil](https://github.com/mastodon/mastodon/pull/30584)) -- Fix encoding detection for link cards ([oneiros](https://github.com/mastodon/mastodon/pull/30780)) -- Fix `/admin/accounts/:account_id/statuses/:id` for edited posts with media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30819)) -- Fix duplicate `@context` attribute in user archive export ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30653)) - -## [4.2.9] - 2024-05-30 - -### Security - -- Update dependencies -- Fix private mention filtering ([GHSA-5fq7-3p3j-9vrf](https://github.com/mastodon/mastodon/security/advisories/GHSA-5fq7-3p3j-9vrf)) -- Fix password change endpoint not being rate-limited ([GHSA-q3rg-xx5v-4mxh](https://github.com/mastodon/mastodon/security/advisories/GHSA-q3rg-xx5v-4mxh)) -- Add hardening around rate-limit bypass ([GHSA-c2r5-cfqr-c553](https://github.com/mastodon/mastodon/security/advisories/GHSA-c2r5-cfqr-c553)) - -### Added - -- Add rate-limit on OAuth application registration ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/30316)) -- Add fallback redirection when getting a webfinger query `WEB_DOMAIN@WEB_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28592)) -- Add `digest` attribute to `Admin::DomainBlock` entity in REST API ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29092)) - -### Removed - -- Remove superfluous application-level caching in some controllers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29862)) -- Remove aggressive OAuth application vacuuming ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/30316)) - -### Fixed - -- Fix leaking Elasticsearch connections in Sidekiq processes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30450)) -- Fix language of remote posts not being recognized when using unusual casing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30403)) -- Fix off-by-one in `tootctl media` commands ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30306)) -- Fix removal of allowed domains (in `LIMITED_FEDERATION_MODE`) not being recorded in the audit log ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/30125)) -- Fix not being able to block a subdomain of an already-blocked domain through the API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30119)) -- Fix `Idempotency-Key` being ignored when scheduling a post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30084)) -- Fix crash when supplying the `FFMPEG_BINARY` environment variable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30022)) -- Fix improper email address validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29838)) -- Fix results/query in `api/v1/featured_tags/suggestions` ([mjankowski](https://github.com/mastodon/mastodon/pull/29597)) -- Fix unblocking internationalized domain names under certain conditions ([tribela](https://github.com/mastodon/mastodon/pull/29530)) -- Fix admin account created by `mastodon:setup` not being auto-approved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29379)) -- Fix reference to non-existent var in CLI maintenance command ([mjankowski](https://github.com/mastodon/mastodon/pull/28363)) - -## [4.2.8] - 2024-02-23 - -### Added - -- Add hourly task to automatically require approval for new registrations in the absence of moderators ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29318), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29355)) - In order to prevent future abandoned Mastodon servers from being used for spam, harassment and other malicious activity, Mastodon will now automatically switch new user registrations to require moderator approval whenever they are left open and no activity (including non-moderation actions from apps) from any logged-in user with permission to access moderation reports has been detected in a full week. - When this happens, users with the permission to change server settings will receive an email notification. - This feature is disabled when `EMAIL_DOMAIN_ALLOWLIST` is used, and can also be disabled with `DISABLE_AUTOMATIC_SWITCHING_TO_APPROVED_REGISTRATIONS=true`. - -### Changed - -- Change registrations to be closed by default on new installations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29280)) - If you are running a server and never changed your registrations mode from the default, updating will automatically close your registrations. - Simply re-enable them through the administration interface or using `tootctl settings registrations open` if you want to enable them again. - -### Fixed - -- Fix processing of remote ActivityPub actors making use of `Link` objects as `Image` `url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29335)) -- Fix link verifications when page size exceeds 1MB ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29358)) - -## [4.2.7] - 2024-02-16 - -### Fixed - -- Fix OmniAuth tests and edge cases in error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29201), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29207)) -- Fix new installs by upgrading to the latest release of the `nsa` gem, instead of a no longer existing commit ([mjankowski](https://github.com/mastodon/mastodon/pull/29065)) - -### Security - -- Fix insufficient checking of remote posts ([GHSA-jhrq-qvrm-qr36](https://github.com/mastodon/mastodon/security/advisories/GHSA-jhrq-qvrm-qr36)) - -## [4.2.6] - 2024-02-14 - -### Security - -- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38)) - In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution. - If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`. - If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`. -- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j)) -- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187)) -- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x)) - In some rare cases, the streaming server was not notified of access tokens revocation on application deletion. -- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3)) - Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address. - This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another. - However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider. - For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable. - In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account. - -## [4.2.5] - 2024-02-01 - -### Security - -- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw)) - -## [4.2.4] - 2024-01-24 - -### Fixed - -- Fix error when processing remote files with unusually long names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28823)) -- Fix processing of compacted single-item JSON-LD collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28816)) -- Retry 401 errors on replies fetching ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28788)) -- Fix `RecordNotUnique` errors in LinkCrawlWorker ([tribela](https://github.com/mastodon/mastodon/pull/28748)) -- Fix Mastodon not correctly processing HTTP Signatures with query strings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28443), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28476)) -- Fix potential redirection loop of streaming endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28665)) -- Fix streaming API redirection ignoring the port of `streaming_api_base_url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28558)) -- Fix error when processing link preview with an array as `inLanguage` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28252)) -- Fix unsupported time zone or locale preventing sign-up ([Gargron](https://github.com/mastodon/mastodon/pull/28035)) -- Fix "Hide these posts from home" list setting not refreshing when switching lists ([brianholley](https://github.com/mastodon/mastodon/pull/27763)) -- Fix missing background behind dismissable banner in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/27479)) -- Fix line wrapping of language selection button with long locale codes ([gunchleoc](https://github.com/mastodon/mastodon/pull/27100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27127)) -- Fix `Undo Announce` activity not being sent to non-follower authors ([MitarashiDango](https://github.com/mastodon/mastodon/pull/18482)) -- Fix N+1s because of association preloaders not actually getting called ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28339)) -- Fix empty column explainer getting cropped under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28337)) -- Fix `LinkCrawlWorker` error when encountering empty OEmbed response ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28268)) -- Fix call to inefficient `delete_matched` cache method in domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28367)) - -### Security - -- Add rate-limit of TOTP authentication attempts at controller level ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28801)) - -## [4.2.3] - 2023-12-05 - -### Fixed - -- Fix dependency on `json-canonicalization` version that has been made unavailable since last release - -## [4.2.2] - 2023-12-04 - -### Changed - -- Change dismissed banners to be stored server-side ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27055)) -- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927)) -- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586)) -- Change single-column navigation notice to be displayed outside of the logo container ([renchap](https://github.com/mastodon/mastodon/pull/27462), [renchap](https://github.com/mastodon/mastodon/pull/27476)) -- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889)) -- Change post language code to include country code when relevant ([gunchleoc](https://github.com/mastodon/mastodon/pull/27099), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27207)) - -### Fixed - -- Fix upper border radius of onboarding columns ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27890)) -- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081)) -- Fix some posts from threads received out-of-order sometimes not being inserted into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27653)) -- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620)) -- Fix error when trying to delete already-deleted file with OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27569)) -- Fix batch attachment deletion when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27554)) -- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474)) -- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459)) -- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442)) -- Fix handling of `inLanguage` attribute in preview card processing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27423)) -- Fix own posts being removed from home timeline when unfollowing a used hashtag ([kmycode](https://github.com/mastodon/mastodon/pull/27391)) -- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584)) -- Fix format-dependent redirects being cached regardless of requested format ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27634)) - -## [4.2.1] - 2023-10-10 - -### Added - -- Add redirection on `/deck` URLs for logged-out users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27128)) -- Add support for v4.2.0 migrations to `tootctl maintenance fix-duplicates` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27147)) - -### Changed - -- Change some worker lock TTLs to be shorter-lived ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27246)) -- Change user archive export allowed period from 7 days to 6 days ([suddjian](https://github.com/mastodon/mastodon/pull/27200)) - -### Fixed - -- Fix duplicate reports being sent when reporting some remote posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27355)) -- Fix clicking on already-opened thread post scrolling to the top of the thread ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27331), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27338), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27350)) -- Fix some remote posts getting truncated ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27307)) -- Fix some cases of infinite scroll code trying to fetch inaccessible posts in a loop ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27286)) -- Fix `Vary` headers not being set on some redirects ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27272)) -- Fix mentions being matched in some URL query strings ([mjankowski](https://github.com/mastodon/mastodon/pull/25656)) -- Fix unexpected linebreak in version string in the Web UI ([vmstan](https://github.com/mastodon/mastodon/pull/26986)) -- Fix double scroll bars in some columns in advanced interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27187)) -- Fix boosts of local users being filtered in account timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27204)) -- Fix multiple instances of the trend refresh scheduler sometimes running at once ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27253)) -- Fix importer returning negative row estimates ([jgillich](https://github.com/mastodon/mastodon/pull/27258)) -- Fix incorrectly keeping outdated update notices absent from the API endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27021)) -- Fix import progress not updating on certain failures ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27247)) -- Fix websocket connections being incorrectly decremented twice on errors ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/27238)) -- Fix explore prompt appearing because of posts being received out of order ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27211)) -- Fix explore prompt sometimes showing up when the home TL is loading ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27062)) -- Fix link handling of mentions in user profiles when logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27185)) -- Fix filtering audit log for entries about disabling 2FA ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27186)) -- Fix notification toasts not respecting reduce-motion ([c960657](https://github.com/mastodon/mastodon/pull/27178)) -- Fix retention dashboard not displaying correct month ([vmstan](https://github.com/mastodon/mastodon/pull/27180)) -- Fix tIME chunk not being properly removed from PNG uploads ([TheEssem](https://github.com/mastodon/mastodon/pull/27111)) -- Fix division by zero in video in bitrate computation code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27129)) -- Fix inefficient queries in “Follows and followers” as well as several admin pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27116), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27306)) -- Fix ActiveRecord using two connection pools when no replica is defined ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27061)) -- Fix the search documentation URL in system checks ([renchap](https://github.com/mastodon/mastodon/pull/27036)) - -## [4.2.0] - 2023-09-21 - -The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by [@danielmbrasil](https://github.com/danielmbrasil), [@mjankowski](https://github.com/mjankowski), [@nschonni](https://github.com/nschonni), [@renchap](https://github.com/renchap), and [@takayamaki](https://github.com/takayamaki). - -### Added - -- **Add full-text search of opted-in public posts and rework search operators** ([Gargron](https://github.com/mastodon/mastodon/pull/26485), [jsgoldstein](https://github.com/mastodon/mastodon/pull/26344), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26657), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26650), [jsgoldstein](https://github.com/mastodon/mastodon/pull/26659), [Gargron](https://github.com/mastodon/mastodon/pull/26660), [Gargron](https://github.com/mastodon/mastodon/pull/26663), [Gargron](https://github.com/mastodon/mastodon/pull/26688), [Gargron](https://github.com/mastodon/mastodon/pull/26689), [Gargron](https://github.com/mastodon/mastodon/pull/26686), [Gargron](https://github.com/mastodon/mastodon/pull/26687), [Gargron](https://github.com/mastodon/mastodon/pull/26692), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26697), [Gargron](https://github.com/mastodon/mastodon/pull/26699), [Gargron](https://github.com/mastodon/mastodon/pull/26701), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26710), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26739), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26754), [Gargron](https://github.com/mastodon/mastodon/pull/26662), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26755), [Gargron](https://github.com/mastodon/mastodon/pull/26781), [Gargron](https://github.com/mastodon/mastodon/pull/26782), [Gargron](https://github.com/mastodon/mastodon/pull/26760), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26756), [Gargron](https://github.com/mastodon/mastodon/pull/26784), [Gargron](https://github.com/mastodon/mastodon/pull/26807), [Gargron](https://github.com/mastodon/mastodon/pull/26835), [Gargron](https://github.com/mastodon/mastodon/pull/26847), [Gargron](https://github.com/mastodon/mastodon/pull/26834), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26893), [tribela](https://github.com/mastodon/mastodon/pull/26896), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26927), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27014)) - This introduces a new `public_statuses` Elasticsearch index for public posts by users who have opted in to their posts being searchable (`toot#indexable` flag). - This also revisits the other indexes to provide more useful indexing, and adds new search operators such as `from:me`, `before:2022-11-01`, `after:2022-11-01`, `during:2022-11-01`, `language:fr`, `has:poll`, or `in:library` (for searching only in posts you have written or interacted with). - Results are now ordered chronologically. -- **Add admin notifications for new Mastodon versions** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26582)) - This is done by querying `https://api.joinmastodon.org/update-check` every 30 minutes in a background job. - That URL can be changed using the `UPDATE_CHECK_URL` environment variable, and the feature outright disabled by setting that variable to an empty string (`UPDATE_CHECK_URL=`). -- **Add “Privacy and reach” tab in profile settings** ([Gargron](https://github.com/mastodon/mastodon/pull/26484), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26508)) - This reorganized scattered privacy and reach settings to a single place, as well as improve their wording. -- **Add display of out-of-band hashtags in the web interface** ([Gargron](https://github.com/mastodon/mastodon/pull/26492), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26497), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26506), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26525), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26606), [Gargron](https://github.com/mastodon/mastodon/pull/26666), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26960)) -- **Add role badges to the web interface** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25649), [Gargron](https://github.com/mastodon/mastodon/pull/26281)) -- **Add ability to pick domains to forward reports to using the `forward_to_domains` parameter in `POST /api/v1/reports`** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25866), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26636)) - The `forward_to_domains` REST API parameter is a list of strings. If it is empty or omitted, the previous behavior is maintained. - The `forward` parameter still needs to be set for `forward_to_domains` to be taken into account. - The forwarded-to domains can only include that of the original author and people being replied to. -- **Add forwarding of reported replies to servers being replied to** ([Gargron](https://github.com/mastodon/mastodon/pull/25341), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26189)) -- Add `ONE_CLICK_SSO_LOGIN` environment variable to directly link to the Single-Sign On provider if there is only one sign up method available ([CSDUMMI](https://github.com/mastodon/mastodon/pull/26083), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26368), [CSDUMMI](https://github.com/mastodon/mastodon/pull/26857), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26901)) -- **Add webhook templating** ([Gargron](https://github.com/mastodon/mastodon/pull/23289)) -- **Add webhooks for local `status.created`, `status.updated`, `account.updated` and `report.updated`** ([VyrCossont](https://github.com/mastodon/mastodon/pull/24133), [VyrCossont](https://github.com/mastodon/mastodon/pull/24243), [VyrCossont](https://github.com/mastodon/mastodon/pull/24211)) -- **Add exclusive lists** ([dariusk, necropolina](https://github.com/mastodon/mastodon/pull/22048), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25324)) -- **Add a confirmation screen when suspending a domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25144), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25603)) -- **Add support for importing lists** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25203), [mgmn](https://github.com/mastodon/mastodon/pull/26120), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26372)) -- **Add optional hCaptcha support** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25019), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25057), [Gargron](https://github.com/mastodon/mastodon/pull/25395), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26388)) -- **Add lines to threads in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24549), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24677), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24696), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24711), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24713), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24715), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24800), [teeerevor](https://github.com/mastodon/mastodon/pull/25706), [renchap](https://github.com/mastodon/mastodon/pull/25807)) -- **Add new onboarding flow to web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24619), [Gargron](https://github.com/mastodon/mastodon/pull/24646), [Gargron](https://github.com/mastodon/mastodon/pull/24705), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24872), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24883), [Gargron](https://github.com/mastodon/mastodon/pull/24954), [stevenjlm](https://github.com/mastodon/mastodon/pull/24959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25010), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25275), [Gargron](https://github.com/mastodon/mastodon/pull/25559), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25561)) -- **Add auto-refresh of accounts we get new messages/edits of** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26510)) -- **Add Elasticsearch cluster health check and indexes mismatch check to dashboard** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26448), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26658)) -- Add `hide_collections`, `discoverable` and `indexable` attributes to credentials API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26998)) -- Add `S3_ENABLE_CHECKSUM_MODE` environment variable to enable checksum verification on compatible S3-providers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26435)) -- Add admin API for managing tags ([rrgeorge](https://github.com/mastodon/mastodon/pull/26872)) -- Add a link to hashtag timelines from the Trending hashtags moderation interface ([gunchleoc](https://github.com/mastodon/mastodon/pull/26724)) -- Add timezone to datetimes in e-mails ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26822)) -- Add `authorized_fetch` server setting in addition to env var ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25798), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26958)) -- Add avatar image to webfinger responses ([tvler](https://github.com/mastodon/mastodon/pull/26558)) -- Add debug logging on signature verification failure ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26637), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26812)) -- Add explicit error messages when DeepL quota is exceeded ([lutoma](https://github.com/mastodon/mastodon/pull/26704)) -- Add Elasticsearch/OpenSearch version to “Software” in admin dashboard ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26652)) -- Add `data-nosnippet` attribute to remote posts and local posts with `noindex` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26648)) -- Add support for federating `memorial` attribute ([rrgeorge](https://github.com/mastodon/mastodon/pull/26583)) -- Add Cherokee and Kalmyk to languages dropdown ([gunchleoc](https://github.com/mastodon/mastodon/pull/26012), [gunchleoc](https://github.com/mastodon/mastodon/pull/26013)) -- Add `DELETE /api/v1/profile/avatar` and `DELETE /api/v1/profile/header` to the REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25124), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26573)) -- Add `ES_PRESET` option to customize numbers of shards and replicas ([Gargron](https://github.com/mastodon/mastodon/pull/26483), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26489)) - This can have a value of `single_node_cluster` (default), `small_cluster` (uses one replica) or `large_cluster` (uses one replica and a higher number of shards). -- Add `CACHE_BUSTER_HTTP_METHOD` environment variable ([renchap](https://github.com/mastodon/mastodon/pull/26528), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26542)) -- Add support for `DB_PASS` when using `DATABASE_URL` ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26295)) -- Add `GET /api/v1/instance/languages` to REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24443)) -- Add primary key to `preview_cards_statuses` join table ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25243), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26384), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26447), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26737), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26979)) -- Add client-side timeout on resend confirmation button ([Gargron](https://github.com/mastodon/mastodon/pull/26300)) -- Add published date and author to news on the explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26155)) -- Add `lang` attribute to various UI components ([c960657](https://github.com/mastodon/mastodon/pull/23869), [c960657](https://github.com/mastodon/mastodon/pull/23891), [c960657](https://github.com/mastodon/mastodon/pull/26111), [c960657](https://github.com/mastodon/mastodon/pull/26149)) -- Add stricter protocol fields validation for accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25937)) -- Add support for Azure blob storage ([mistydemeo](https://github.com/mastodon/mastodon/pull/23607), [mistydemeo](https://github.com/mastodon/mastodon/pull/26080)) -- Add toast with option to open post after publishing in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25564), [Signez](https://github.com/mastodon/mastodon/pull/25919), [Gargron](https://github.com/mastodon/mastodon/pull/26664)) -- Add canonical link tags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25715)) -- Add button to see results for polls in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25726)) -- Add at-symbol prepended to mention span title ([forsamori](https://github.com/mastodon/mastodon/pull/25684)) -- Add users index on `unconfirmed_email` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25672), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25702)) -- Add superapp index on `oauth_applications` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25670)) -- Add index to backups on `user_id` column ([mjankowski](https://github.com/mastodon/mastodon/pull/25647)) -- Add onboarding prompt when home feed too slow in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25267), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25556), [Gargron](https://github.com/mastodon/mastodon/pull/25579), [renchap](https://github.com/mastodon/mastodon/pull/25580), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25581), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25617), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25917), [Gargron](https://github.com/mastodon/mastodon/pull/26829), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26935)) -- Add `POST /api/v1/conversations/:id/unread` API endpoint to mark a conversation as unread ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25509)) -- Add `translate="no"` to outgoing mentions and links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25524)) -- Add unsubscribe link and headers to e-mails ([Gargron](https://github.com/mastodon/mastodon/pull/25378), [c960657](https://github.com/mastodon/mastodon/pull/26085)) -- Add logging of websocket send errors ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25280)) -- Add time zone preference ([Gargron](https://github.com/mastodon/mastodon/pull/25342), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26025)) -- Add `legal` as report category ([Gargron](https://github.com/mastodon/mastodon/pull/23941), [renchap](https://github.com/mastodon/mastodon/pull/25400), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26509)) -- Add `data-nosnippet` so Google doesn't use trending posts in snippets for `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25279)) -- Add card with who invited you to join when displaying rules on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23475)) -- Add missing primary keys to `accounts_tags` and `statuses_tags` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25210)) -- Add support for custom sign-up URLs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25014), [renchap](https://github.com/mastodon/mastodon/pull/25108), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25190), [mgmn](https://github.com/mastodon/mastodon/pull/25531)) - This is set using `SSO_ACCOUNT_SIGN_UP` and reflected in the REST API by adding `registrations.sign_up_url` to the `/api/v2/instance` endpoint. -- Add polling and automatic redirection to `/start` on email confirmation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25013)) -- Add ability to block sign-ups from IP using the CLI ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24870)) -- Add ALT badges to media that has alternative text in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24782), [c960657](https://github.com/mastodon/mastodon/pull/26166) -- Add ability to include accounts with pending follow requests in lists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19727), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24810)) -- Add trend management to admin API ([rrgeorge](https://github.com/mastodon/mastodon/pull/24257)) - - `POST /api/v1/admin/trends/statuses/:id/approve` - - `POST /api/v1/admin/trends/statuses/:id/reject` - - `POST /api/v1/admin/trends/links/:id/approve` - - `POST /api/v1/admin/trends/links/:id/reject` - - `POST /api/v1/admin/trends/tags/:id/approve` - - `POST /api/v1/admin/trends/tags/:id/reject` - - `GET /api/v1/admin/trends/links/publishers` - - `POST /api/v1/admin/trends/links/publishers/:id/approve` - - `POST /api/v1/admin/trends/links/publishers/:id/reject` -- Add user handle to notification mail recipient address ([HeitorMC](https://github.com/mastodon/mastodon/pull/24240)) -- Add progress indicator to sign-up flow ([Gargron](https://github.com/mastodon/mastodon/pull/24545)) -- Add client-side validation for taken username in sign-up form ([Gargron](https://github.com/mastodon/mastodon/pull/24546)) -- Add `--approve` option to `tootctl accounts create` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24533)) -- Add “In Memoriam” banner back to profiles ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23591), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23614)) - This adds the `memorial` attribute to the `Account` REST API entity. -- Add colour to follow button when hashtag is being followed ([c960657](https://github.com/mastodon/mastodon/pull/24361)) -- Add further explanations to the profile link verification instructions ([drzax](https://github.com/mastodon/mastodon/pull/19723)) -- Add a link to Identity provider's account settings from the account settings ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24628)) -- Add support for streaming server to connect to postgres with self-signed certs through the `sslmode` URL parameter ([ramuuns](https://github.com/mastodon/mastodon/pull/21431)) -- Add support for specifying S3 storage classes through the `S3_STORAGE_CLASS` environment variable ([hyl](https://github.com/mastodon/mastodon/pull/22480)) -- Add support for incoming rich text ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23913)) -- Add support for Ruby 3.2 ([tenderlove](https://github.com/mastodon/mastodon/pull/22928), [casperisfine](https://github.com/mastodon/mastodon/pull/24142), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24202), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26934)) -- Add API parameter to safeguard unexpected mentions in new posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18350)) - -### Changed - -- **Change hashtags to be displayed separately when they are the last line of a post** ([renchap](https://github.com/mastodon/mastodon/pull/26499), [renchap](https://github.com/mastodon/mastodon/pull/26614), [renchap](https://github.com/mastodon/mastodon/pull/26615)) -- **Change reblogs to be excluded from "Posts and replies" tab in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26302)) -- **Change interaction modal in web interface** ([Gargron, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26075), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26269), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26268), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26267), [mgmn](https://github.com/mastodon/mastodon/pull/26459), [tribela](https://github.com/mastodon/mastodon/pull/26461), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26593), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26795)) -- **Change design of link previews in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26136), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26151), [Gargron](https://github.com/mastodon/mastodon/pull/26153), [Gargron](https://github.com/mastodon/mastodon/pull/26250), [Gargron](https://github.com/mastodon/mastodon/pull/26287), [Gargron](https://github.com/mastodon/mastodon/pull/26286), [c960657](https://github.com/mastodon/mastodon/pull/26184)) -- **Change "direct message" nomenclature to "private mention" in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24248)) -- **Change translation feature to cover Content Warnings, poll options and media descriptions** ([c960657](https://github.com/mastodon/mastodon/pull/24175), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25251), [c960657](https://github.com/mastodon/mastodon/pull/26168), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26452)) -- **Change account search to match by text when opted-in** ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25599), [Gargron](https://github.com/mastodon/mastodon/pull/26378)) -- **Change import feature to be clearer, less error-prone and more reliable** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21054), [mgmn](https://github.com/mastodon/mastodon/pull/24874)) -- **Change local and federated timelines to be tabs of a single “Live feeds” column** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25641), [Gargron](https://github.com/mastodon/mastodon/pull/25683), [mgmn](https://github.com/mastodon/mastodon/pull/25694), [Plastikmensch](https://github.com/mastodon/mastodon/pull/26247), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26633)) -- **Change user archive export to be faster and more reliable, and export `.zip` archives instead of `.tar.gz` ones** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23360), [TheEssem](https://github.com/mastodon/mastodon/pull/25034)) -- **Change `mastodon-streaming` systemd unit files to be templated** ([e-nomem](https://github.com/mastodon/mastodon/pull/24751)) -- **Change `statsd` integration to disable sidekiq metrics by default** ([mjankowski](https://github.com/mastodon/mastodon/pull/25265), [mjankowski](https://github.com/mastodon/mastodon/pull/25336), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26310)) - This deprecates `statsd` support and disables the sidekiq integration unless `STATSD_SIDEKIQ` is set to `true`. - This is because the `nsa` gem is unmaintained, and its sidekiq integration is known to add very significant overhead. - Later versions of Mastodon will have other ways to get the same metrics. -- **Change replica support to native Rails adapter** ([krainboltgreene](https://github.com/mastodon/mastodon/pull/25693), [Gargron](https://github.com/mastodon/mastodon/pull/25849), [Gargron](https://github.com/mastodon/mastodon/pull/25874), [Gargron](https://github.com/mastodon/mastodon/pull/25851), [Gargron](https://github.com/mastodon/mastodon/pull/25977), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26074), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26326), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26386), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26856)) - This is a breaking change, dropping `makara` support, and requiring you to update your database configuration if you are using replicas. - To tell Mastodon to use a read replica, you can either set the `REPLICA_DB_NAME` environment variable (along with `REPLICA_DB_USER`, `REPLICA_DB_PASS`, `REPLICA_DB_HOST`, and `REPLICA_DB_PORT`, if they differ from the primary database), or the `REPLICA_DATABASE_URL` environment variable if your configuration is based on `DATABASE_URL`. -- Change DCT method used for JPEG encoding to float ([electroCutie](https://github.com/mastodon/mastodon/pull/26675)) -- Change from `node-redis` to `ioredis` for streaming ([gmemstr](https://github.com/mastodon/mastodon/pull/26581)) -- Change private statuses index to index without crutches ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26713)) -- Change video compression parameters ([Gargron](https://github.com/mastodon/mastodon/pull/26631), [Gargron](https://github.com/mastodon/mastodon/pull/26745), [Gargron](https://github.com/mastodon/mastodon/pull/26766), [Gargron](https://github.com/mastodon/mastodon/pull/26970)) -- Change admin e-mail notification settings to be their own settings group ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26596)) -- Change opacity of the delete icon in the search field to be more visible ([AntoninDelFabbro](https://github.com/mastodon/mastodon/pull/26449)) -- Change Account Search to prioritize username over display name ([jsgoldstein](https://github.com/mastodon/mastodon/pull/26623)) -- Change follow recommendation materialized view to be faster in most cases ([renchap, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26545)) -- Change `robots.txt` to block GPTBot ([Foritus](https://github.com/mastodon/mastodon/pull/26396)) -- Change header of hashtag timelines in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26362), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26416)) -- Change streaming `/metrics` to include additional metrics ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26299), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26945)) -- Change indexing frequency from 5 minutes to 1 minute, add locks to schedulers ([Gargron](https://github.com/mastodon/mastodon/pull/26304)) -- Change column link to add a better keyboard focus indicator ([teeerevor](https://github.com/mastodon/mastodon/pull/26278)) -- Change poll form element colors to fit with the rest of the ui ([teeerevor](https://github.com/mastodon/mastodon/pull/26139), [teeerevor](https://github.com/mastodon/mastodon/pull/26162), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26164)) -- Change 'favourite' to 'favorite' for American English ([marekr](https://github.com/mastodon/mastodon/pull/24667), [gunchleoc](https://github.com/mastodon/mastodon/pull/26009), [nabijaczleweli](https://github.com/mastodon/mastodon/pull/26109)) -- Change ActivityStreams representation of suspended accounts to not use a blank `name` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25276)) -- Change focus UI for keyboard only input ([teeerevor](https://github.com/mastodon/mastodon/pull/25935), [Gargron](https://github.com/mastodon/mastodon/pull/26125), [Gargron](https://github.com/mastodon/mastodon/pull/26767)) -- Change thread view to scroll to the selected post rather than the post being replied to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24685)) -- Change links in multi-column mode so tabs are open in single-column mode ([Signez](https://github.com/mastodon/mastodon/pull/25893), [Signez](https://github.com/mastodon/mastodon/pull/26070), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25973), [Signez](https://github.com/mastodon/mastodon/pull/26019), [Signez](https://github.com/mastodon/mastodon/pull/26759)) -- Change searching with `#` to include account index ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25638)) -- Change label and design of sensitive and unavailable media in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25712), [Gargron](https://github.com/mastodon/mastodon/pull/26135), [Gargron](https://github.com/mastodon/mastodon/pull/26330)) -- Change button colors to increase hover/focus contrast and consistency ([teeerevor](https://github.com/mastodon/mastodon/pull/25677), [Gargron](https://github.com/mastodon/mastodon/pull/25679)) -- Change dropdown icon above compose form from ellipsis to bars in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25661)) -- Change header backgrounds to use fewer different colors in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25577)) -- Change files to be deleted in batches instead of one-by-one ([Gargron](https://github.com/mastodon/mastodon/pull/23302), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25586), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25587)) -- Change emoji picker icon ([iparr](https://github.com/mastodon/mastodon/pull/25479)) -- Change edit profile page ([Gargron](https://github.com/mastodon/mastodon/pull/25413), [c960657](https://github.com/mastodon/mastodon/pull/26538)) -- Change "bot" label to "automated" ([Gargron](https://github.com/mastodon/mastodon/pull/25356)) -- Change design of dropdowns in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25107)) -- Change wording of “Content cache retention period” setting to highlight destructive implications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23261)) -- Change autolinking to allow carets in URL search params ([renchap](https://github.com/mastodon/mastodon/pull/25216)) -- Change share action from being in action bar to being in dropdown in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25105)) -- Change sessions to be ordered from most-recent to least-recently updated ([frankieroberto](https://github.com/mastodon/mastodon/pull/25005)) -- Change vacuum scheduler to also delete expired tokens and unused application records ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24871)) -- Change "Sign in" to "Login" ([Gargron](https://github.com/mastodon/mastodon/pull/24942)) -- Change domain suspensions to also be checked before trying to fetch unknown remote resources ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24535)) -- Change media components to use aspect-ratio rather than compute height themselves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24686), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24943), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26801)) -- Change logo version in header based on screen size in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24707)) -- Change label from "For you" to "People" on explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24706)) -- Change logged-out WebUI HTML pages to be cached for a few seconds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24708)) -- Change unauthenticated responses to be cached in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/24348), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24662), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24665)) -- Change HTTP caching logic ([Gargron](https://github.com/mastodon/mastodon/pull/24347), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24604)) -- Change hashtags and mentions in bios to open in-app in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24643)) -- Change styling of the recommended accounts to allow bio to be more visible ([chike00](https://github.com/mastodon/mastodon/pull/24480)) -- Change account search in moderation interface to allow searching by username including the leading `@` ([HeitorMC](https://github.com/mastodon/mastodon/pull/24242)) -- Change all components to use the same error page in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24512)) -- Change search pop-out in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24305)) -- Change user settings to be stored in a more optimal way ([Gargron](https://github.com/mastodon/mastodon/pull/23630), [c960657](https://github.com/mastodon/mastodon/pull/24321), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24460), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24558), [Gargron](https://github.com/mastodon/mastodon/pull/24761), [Gargron](https://github.com/mastodon/mastodon/pull/24783), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25508), [jsgoldstein](https://github.com/mastodon/mastodon/pull/25340), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26884), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27012)) -- Change media upload limits and remove client-side resizing ([Gargron](https://github.com/mastodon/mastodon/pull/23726)) -- Change design of account rows in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24247), [Gargron](https://github.com/mastodon/mastodon/pull/24343), [Gargron](https://github.com/mastodon/mastodon/pull/24956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25131)) -- Change log-out to use Single Logout when using external log-in through OIDC ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24020)) -- Change sidekiq-bulk's batch size from 10,000 to 1,000 jobs in one Redis call ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24034)) -- Change translation to only be offered for supported languages ([c960657](https://github.com/mastodon/mastodon/pull/23879), [c960657](https://github.com/mastodon/mastodon/pull/24037)) - This adds the `/api/v1/instance/translation_languages` REST API endpoint that returns an object with the supported translation language pairs in the form: - ```json - { - "fr": ["en", "de"] - } - ``` - (where `fr` is a supported source language and `en` and `de` or supported output language when translating a `fr` string) -- Change compose form checkbox to native input with `appearance: none` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22949)) -- Change posts' clickable area to be larger ([c960657](https://github.com/mastodon/mastodon/pull/23621)) -- Change `followed_by` link to `location=all` if account is local on /admin/accounts/:id page ([tribela](https://github.com/mastodon/mastodon/pull/23467)) - -### Removed - -- **Remove support for Node.js 14** ([renchap](https://github.com/mastodon/mastodon/pull/25198)) -- **Remove support for Ruby 2.7** ([nschonni](https://github.com/mastodon/mastodon/pull/24237)) -- **Remove clustering from streaming API** ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24655)) -- **Remove anonymous access to the streaming API** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23989)) -- Remove obfuscation of reply count in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26768)) -- Remove `kmr` from language selection, as it was a duplicate for `ku` ([gunchleoc](https://github.com/mastodon/mastodon/pull/26014), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26787)) -- Remove 16:9 cropping from web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26132)) -- Remove back button from bookmarks, favourites and lists screens in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26126)) -- Remove display name input from sign-up form ([Gargron](https://github.com/mastodon/mastodon/pull/24704)) -- Remove `tai` locale ([c960657](https://github.com/mastodon/mastodon/pull/23880)) -- Remove empty Kushubian (csb) local files ([nschonni](https://github.com/mastodon/mastodon/pull/24151)) -- Remove `Permissions-Policy` header from all responses ([Gargron](https://github.com/mastodon/mastodon/pull/24124)) - -### Fixed - -- **Fix filters not being applying in the explore page** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25887)) -- **Fix being unable to load past a full page of filtered posts in Home timeline** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24930)) -- **Fix log-in flow when involving both OAuth and external authentication** ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24073)) -- **Fix broken links in account gallery** ([c960657](https://github.com/mastodon/mastodon/pull/24218)) -- **Fix migration handler not updating lists** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24808)) -- Fix crash when viewing a moderation appeal and the moderator account has been deleted ([xrobau](https://github.com/mastodon/mastodon/pull/25900)) -- Fix error in Web UI when server rules cannot be fetched ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26957)) -- Fix paragraph margins resulting in irregular read-more cut-off in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26828)) -- Fix notification permissions being requested immediately after login ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26472)) -- Fix performances of profile directory ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26840), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26842)) -- Fix mute button and volume slider feeling disconnected in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26827), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26860)) -- Fix “Scoped order is ignored, it's forced to be batch order.” warnings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26793)) -- Fix blocked domain appearing in account feeds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26823)) -- Fix invalid `Content-Type` header for WebP images ([c960657](https://github.com/mastodon/mastodon/pull/26773)) -- Fix minor inefficiencies in `tootctl search deploy` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26721)) -- Fix filter form in profiles directory overflowing instead of wrapping ([arbolitoloco1](https://github.com/mastodon/mastodon/pull/26682)) -- Fix sign up steps progress layout in right-to-left locales ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26728)) -- Fix bug with “favorited by” and “reblogged by“ view on posts only showing up to 40 items ([timothyjrogers](https://github.com/mastodon/mastodon/pull/26577), [timothyjrogers](https://github.com/mastodon/mastodon/pull/26574)) -- Fix bad search type heuristic ([Gargron](https://github.com/mastodon/mastodon/pull/26673)) -- Fix not being able to negate prefix clauses in search ([Gargron](https://github.com/mastodon/mastodon/pull/26672)) -- Fix timeout on invalid set of exclusionary parameters in `/api/v1/timelines/public` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/26239)) -- Fix adding column with default value taking longer on Postgres >= 11 ([Gargron](https://github.com/mastodon/mastodon/pull/26375)) -- Fix light theme select option for hashtags ([teeerevor](https://github.com/mastodon/mastodon/pull/26311)) -- Fix AVIF attachments ([c960657](https://github.com/mastodon/mastodon/pull/26264)) -- Fix incorrect URL normalization when fetching remote resources ([c960657](https://github.com/mastodon/mastodon/pull/26219), [c960657](https://github.com/mastodon/mastodon/pull/26285)) -- Fix being unable to filter posts for individual Chinese languages ([gunchleoc](https://github.com/mastodon/mastodon/pull/26066)) -- Fix preview card sometimes linking to 4xx error pages ([c960657](https://github.com/mastodon/mastodon/pull/26200)) -- Fix emoji picker button scrolling with textarea content in single-column view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25304)) -- Fix missing border on error screen in light theme in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26152)) -- Fix UI overlap with the loupe icon in the Explore Tab ([gol-cha](https://github.com/mastodon/mastodon/pull/26113)) -- Fix unexpected redirection to `/explore` after sign-in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26143)) -- Fix `/api/v1/statuses/:id/unfavourite` and `/api/v1/statuses/:id/unreblog` returning non-updated counts ([c960657](https://github.com/mastodon/mastodon/pull/24365)) -- Fix clicking the “Back” button sometimes leading out of Mastodon ([c960657](https://github.com/mastodon/mastodon/pull/23953), [CSFlorin](https://github.com/mastodon/mastodon/pull/24835), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/24867), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25281)) -- Fix processing of `null` ActivityPub activities ([tribela](https://github.com/mastodon/mastodon/pull/26021)) -- Fix hashtag posts not being removed from home feed on hashtag unfollow ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26028)) -- Fix for "follows you" indicator in light web UI not readable ([vmstan](https://github.com/mastodon/mastodon/pull/25993)) -- Fix incorrect line break between icon and number of reposts & favourites ([edent](https://github.com/mastodon/mastodon/pull/26004)) -- Fix sounds not being loaded from assets host ([Signez](https://github.com/mastodon/mastodon/pull/25931)) -- Fix buttons showing inconsistent styles ([teeerevor](https://github.com/mastodon/mastodon/pull/25903), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25965), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26341), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26482)) -- Fix trend calculation working on too many items at a time ([Gargron](https://github.com/mastodon/mastodon/pull/25835)) -- Fix dropdowns being disabled for logged out users in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25964)) -- Fix explore page being inaccessible when opted-out of trends in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25716)) -- Fix re-activated accounts possibly getting deleted by `AccountDeletionWorker` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25711)) -- Fix `/api/v2/search` not working with following query param ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25681)) -- Fix inefficient query when requesting a new confirmation email from a logged-in account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25669)) -- Fix unnecessary concurrent calls to `/api/*/instance` in web UI ([mgmn](https://github.com/mastodon/mastodon/pull/25663)) -- Fix resolving local URL for remote content ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637)) -- Fix search not being easily findable on smaller screens in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25576), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25631)) -- Fix j/k keyboard shortcuts on some status lists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25554)) -- Fix missing validation on `default_privacy` setting ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25513)) -- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477)) -- Fix non-interactive upload container being given a `button` role and tabIndex ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25462)) -- Fix always redirecting to onboarding in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25396)) -- Fix inconsistent use of middle dot (·) instead of bullet (•) to separate items ([j-f1](https://github.com/mastodon/mastodon/pull/25248)) -- Fix spacing of middle dots in the detailed status meta section ([j-f1](https://github.com/mastodon/mastodon/pull/25247)) -- Fix prev/next buttons color in media viewer ([renchap](https://github.com/mastodon/mastodon/pull/25231)) -- Fix email addresses not being properly updated in `tootctl maintenance fix-duplicates` ([mjankowski](https://github.com/mastodon/mastodon/pull/25118)) -- Fix unicode surrogate pairs sometimes being broken in page title ([eai04191](https://github.com/mastodon/mastodon/pull/25148)) -- Fix various inefficient queries against account domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25126)) -- Fix video player offering to expand in a lightbox when it's in an `iframe` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25067)) -- Fix post embed previews ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25071)) -- Fix inadequate error handling in several API controllers when given invalid parameters ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24947), [danielmbrasil](https://github.com/mastodon/mastodon/pull/24958), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25063), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25072), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25386), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25595)) -- Fix uncaught `ActiveRecord::StatementInvalid` in Mastodon::IpBlocksCLI ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24861)) -- Fix various edge cases with local moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24812)) -- Fix `tootctl accounts cull` crashing when encountering a domain resolving to a private address ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23378)) -- Fix `tootctl accounts approve --number N` not aproving the N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605)) -- Fix being unable to clear media description when editing posts ([c960657](https://github.com/mastodon/mastodon/pull/24720)) -- Fix unavailable translations not falling back to English ([mgmn](https://github.com/mastodon/mastodon/pull/24727)) -- Fix anonymous visitors getting a session cookie on first visit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24584), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24650), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24664)) -- Fix cutting off first letter of hashtag links sometimes in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24623)) -- Fix crash in `tootctl accounts create --reattach --force` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24557), [danielmbrasil](https://github.com/mastodon/mastodon/pull/24680)) -- Fix characters being emojified even when using Variation Selector 15 (text) ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20949), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24615)) -- Fix uncaught ActiveRecord::StatementInvalid exception in `Mastodon::AccountsCLI#approve` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24590)) -- Fix email confirmation skip option in `tootctl accounts modify USERNAME --email EMAIL --confirm` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24578)) -- Fix tooltip for dates without time ([c960657](https://github.com/mastodon/mastodon/pull/24244)) -- Fix missing loading spinner and loading more on scroll in Private Mentions column ([c960657](https://github.com/mastodon/mastodon/pull/24446)) -- Fix account header image missing from `/settings/profile` on narrow screens ([c960657](https://github.com/mastodon/mastodon/pull/24433)) -- Fix height of announcements not being updated when using reduced animations ([c960657](https://github.com/mastodon/mastodon/pull/24354)) -- Fix inconsistent radius in advanced interface drawer ([thislight](https://github.com/mastodon/mastodon/pull/24407)) -- Fix loading more trending posts on scroll in the advanced interface ([OmmyZhang](https://github.com/mastodon/mastodon/pull/24314)) -- Fix poll ending notification for edited polls ([c960657](https://github.com/mastodon/mastodon/pull/24311)) -- Fix max width of media in `/about` and `/privacy-policy` ([mgmn](https://github.com/mastodon/mastodon/pull/24180)) -- Fix streaming API not being usable without `DATABASE_URL` ([Gargron](https://github.com/mastodon/mastodon/pull/23960)) -- Fix external authentication not running onboarding code for new users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23458)) - -## [4.1.8] - 2023-09-19 - -### Fixed - -- Fix post edits not being forwarded as expected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26936)) -- Fix moderator rights inconsistencies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26729)) -- Fix crash when encountering invalid URL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26814)) -- Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409)) -- Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500)) -- Fix unexpected audio stream transcoding when uploaded video is eligible to passthrough ([yufushiro](https://github.com/mastodon/mastodon/pull/26608)) - -### Security - -- Fix missing HTML sanitization in translation API (CVE-2023-42452, [GHSA-2693-xr3m-jhqr](https://github.com/mastodon/mastodon/security/advisories/GHSA-2693-xr3m-jhqr)) -- Fix incorrect domain name normalization (CVE-2023-42451, [GHSA-v3xf-c9qf-j667](https://github.com/mastodon/mastodon/security/advisories/GHSA-v3xf-c9qf-j667)) - -## [4.1.7] - 2023-09-05 - -### Changed - -- Change remote report processing to accept reports with long comments, but truncate them ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25028)) - -### Fixed - -- **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392)) -- Fix `/api/v1/timelines/tag/:hashtag` allowing for unauthenticated access when public preview is disabled ([danielmbrasil](https://github.com/mastodon/mastodon/pull/26237)) -- Fix inefficiencies in `PlainTextFormatter` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26727)) - -## [4.1.6] - 2023-07-31 - -### Fixed - -- Fix memory leak in streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26228)) -- Fix wrong filters sometimes applying in streaming ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26159), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26213), [renchap](https://github.com/mastodon/mastodon/pull/26233)) -- Fix incorrect connect timeout in outgoing requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26116)) - -## [4.1.5] - 2023-07-21 - -### Added - -- Add check preventing Sidekiq workers from running with Makara configured ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25850)) - -### Changed - -- Change request timeout handling to use a longer deadline ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26055)) - -### Fixed - -- Fix moderation interface for remote instances with a .zip TLD ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25885)) -- Fix remote accounts being possibly persisted to database with incomplete protocol values ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25886)) -- Fix trending publishers table not rendering correctly on narrow screens ([vmstan](https://github.com/mastodon/mastodon/pull/25945)) - -### Security - -- Fix CSP headers being unintentionally wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26105)) - -## [4.1.4] - 2023-07-07 - -### Fixed - -- Fix branding:generate_app_icons failing because of disallowed ICO coder ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25794)) -- Fix crash in admin interface when viewing a remote user with verified links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25796)) -- Fix processing of media files with unusual names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25788)) - -## [4.1.3] - 2023-07-06 - -### Added - -- Add fallback redirection when getting a webfinger query `LOCAL_DOMAIN@LOCAL_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23600)) - -### Changed - -- Change OpenGraph-based embeds to allow fullscreen ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25058)) -- Change AccessTokensVacuum to also delete expired tokens ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868)) -- Change profile updates to be sent to recently-mentioned servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24852)) -- Change automatic post deletion thresholds and load detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24614)) -- Change `/api/v1/statuses/:id/history` to always return at least one item ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25510)) -- Change auto-linking to allow carets in URL query params ([renchap](https://github.com/mastodon/mastodon/pull/25216)) - -### Removed - -- Remove invalid `X-Frame-Options: ALLOWALL` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25070)) - -### Fixed - -- Fix wrong view being displayed when a webhook fails validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25464)) -- Fix soft-deleted post cleanup scheduler overwhelming the streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25519)) -- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477)) -- Fix multiple inefficiencies in automatic post cleanup worker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24785), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24840)) -- Fix performance of streaming by parsing message JSON once ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25278), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25361)) -- Fix CSP headers when `S3_ALIAS_HOST` includes a path component ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25273)) -- Fix `tootctl accounts approve --number N` not approving N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605)) -- Fix reports not being closed when performing batch suspensions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24988)) -- Fix being able to vote on your own polls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25015)) -- Fix race condition when reblogging a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25016)) -- Fix “Authorized applications” inefficiently and incorrectly getting last use date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25060)) -- Fix “Authorized applications” crashing when listing apps with certain admin API scopes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25713)) -- Fix multiple N+1s in ConversationsController ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25134), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25399), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25499)) -- Fix user archive takeouts when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24431)) -- Fix searching for remote content by URL not working under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637)) -- Fix inefficiencies in indexing content for search ([VyrCossont](https://github.com/mastodon/mastodon/pull/24285), [VyrCossont](https://github.com/mastodon/mastodon/pull/24342)) - -### Security - -- Add finer permission requirements for managing webhooks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25463)) -- Update dependencies -- Add hardening headers for user-uploaded files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25756)) -- Fix verified links possibly hiding important parts of the URL (CVE-2023-36462) -- Fix timeout handling of outbound HTTP requests (CVE-2023-36461) -- Fix arbitrary file creation through media processing (CVE-2023-36460) -- Fix possible XSS in preview cards (CVE-2023-36459) - -## [4.1.2] - 2023-04-04 - -### Fixed - -- Fix crash in `tootctl` commands making use of parallelization when Elasticsearch is enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24182), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24377)) -- Fix crash in `db:setup` when Elasticsearch is enabled ([rrgeorge](https://github.com/mastodon/mastodon/pull/24302)) -- Fix user archive takeout when using OpenStack Swift or S3 providers with no ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24200)) -- Fix invalid/expired invites being processed on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24337)) - -### Security - -- Update Ruby to 3.0.6 due to ReDoS vulnerabilities ([saizai](https://github.com/mastodon/mastodon/pull/24334)) -- Fix unescaped user input in LDAP query ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24379)) - -## [4.1.1] - 2023-03-16 - -### Added - -- Add redirection from paths with url-encoded `@` to their decoded form ([thijskh](https://github.com/mastodon/mastodon/pull/23593)) -- Add `lang` attribute to native language names in language picker in Web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23749)) -- Add headers to outgoing mails to avoid auto-replies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23597)) -- Add support for refreshing many accounts at once with `tootctl accounts refresh` ([9p4](https://github.com/mastodon/mastodon/pull/23304)) -- Add confirmation modal when clicking to edit a post with a non-empty compose form ([PauloVilarinho](https://github.com/mastodon/mastodon/pull/23936)) -- Add support for the HAproxy PROXY protocol through the `PROXY_PROTO_V1` environment variable ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24064)) -- Add `SENDFILE_HEADER` environment variable ([Gargron](https://github.com/mastodon/mastodon/pull/24123)) -- Add cache headers to static files served through Rails ([Gargron](https://github.com/mastodon/mastodon/pull/24120)) - -### Changed - -- Increase contrast of upload progress bar background ([toolmantim](https://github.com/mastodon/mastodon/pull/23836)) -- Change post auto-deletion throttling constants to better scale with server size ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23320)) -- Change order of bookmark and favourite sidebar entries in single-column UI for consistency ([TerryGarcia](https://github.com/mastodon/mastodon/pull/23701)) -- Change `ActivityPub::DeliveryWorker` retries to be spread out more ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21956)) - -### Fixed - -- Fix “Remove all followers from the selected domains” also removing follows and notifications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23805)) -- Fix streaming metrics format ([emilweth](https://github.com/mastodon/mastodon/pull/23519), [emilweth](https://github.com/mastodon/mastodon/pull/23520)) -- Fix case-sensitive check for previously used hashtags in hashtag autocompletion ([deanveloper](https://github.com/mastodon/mastodon/pull/23526)) -- Fix focus point of already-attached media not saving after edit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23566)) -- Fix sidebar behavior in settings/admin UI on mobile ([wxt2005](https://github.com/mastodon/mastodon/pull/23764)) -- Fix inefficiency when searching accounts per username in admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23801)) -- Fix duplicate “Publish” button on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23804)) -- Fix server error when failing to follow back followers from `/relationships` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23787)) -- Fix server error when attempting to display the edit history of a trendable post in the admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23574)) -- Fix `tootctl accounts migrate` crashing because of a typo ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23567)) -- Fix original account being unfollowed on migration before the follow request to the new account could be sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21957)) -- Fix the “Back” button in column headers sometimes leaving Mastodon ([c960657](https://github.com/mastodon/mastodon/pull/23953)) -- Fix pgBouncer resetting application name on every transaction ([Gargron](https://github.com/mastodon/mastodon/pull/23958)) -- Fix unconfirmed accounts being counted as active users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23803)) -- Fix `/api/v1/streaming` sub-paths not being redirected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23988)) -- Fix drag'n'drop upload area text that spans multiple lines not being centered ([vintprox](https://github.com/mastodon/mastodon/pull/24029)) -- Fix sidekiq jobs not triggering Elasticsearch index updates ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24046)) -- Fix tags being unnecessarily stripped from plain-text short site description ([c960657](https://github.com/mastodon/mastodon/pull/23975)) -- Fix HTML entities not being un-escaped in extracted plain-text from remote posts ([c960657](https://github.com/mastodon/mastodon/pull/24019)) -- Fix dashboard crash on ElasticSearch server error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23751)) -- Fix incorrect post links in strikes when the account is remote ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23611)) -- Fix misleading error code when receiving invalid WebAuthn credentials ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23568)) -- Fix duplicate mails being sent when the SMTP server is too slow to close the connection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23750)) - -### Security - -- Change user backups to use expiring URLs for download when possible ([Gargron](https://github.com/mastodon/mastodon/pull/24136)) -- Add warning for object storage misconfiguration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24137)) - -## [4.1.0] - 2023-02-10 - -### Added - -- **Add support for importing/exporting server-wide domain blocks** ([enbylenore](https://github.com/mastodon/mastodon/pull/20597), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21471), [dariusk](https://github.com/mastodon/mastodon/pull/22803), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21470)) -- **Add listing of followed hashtags** ([connorshea](https://github.com/mastodon/mastodon/pull/21773)) -- **Add support for editing media description and focus point of already-sent posts** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20878)) - - Previously, you could add and remove attachments, but not edit media description of already-attached media - - REST API changes: - - `PUT /api/v1/statuses/:id` now takes an extra `media_attributes[]` array parameter with the `id` of the updated media and their updated `description`, `focus`, and `thumbnail` -- **Add follow request banner on account header** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20785)) - - REST API changes: - - `Relationship` entities have an extra `requested_by` boolean attribute representing whether the represented user has requested to follow you -- **Add confirmation screen when handling reports** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22375), [Gargron](https://github.com/mastodon/mastodon/pull/23156), [tribela](https://github.com/mastodon/mastodon/pull/23178)) -- Add option to make the landing page be `/about` even when trends are enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20808)) -- Add `noindex` setting back to the admin interface ([prplecake](https://github.com/mastodon/mastodon/pull/22205)) -- Add instance peers API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22810)) -- Add instance activity API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22833)) -- Add setting for status page URL ([Gargron](https://github.com/mastodon/mastodon/pull/23390), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23499)) - - REST API changes: - - Add `configuration.urls.status` attribute to the object returned by `GET /api/v2/instance` -- Add `account.approved` webhook ([Saiv46](https://github.com/mastodon/mastodon/pull/22938)) -- Add 12 hours option to polls ([Pleclown](https://github.com/mastodon/mastodon/pull/21131)) -- Add dropdown menu item to open admin interface for remote domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21895)) -- Add `--remove-headers`, `--prune-profiles` and `--include-follows` flags to `tootctl media remove` ([evanphilip](https://github.com/mastodon/mastodon/pull/22149)) -- Add `--email` and `--dry-run` options to `tootctl accounts delete` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22328)) -- Add `tootctl accounts migrate` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22330)) -- Add `tootctl accounts prune` ([tribela](https://github.com/mastodon/mastodon/pull/18397)) -- Add `tootctl domains purge` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22063)) -- Add `SIDEKIQ_CONCURRENCY` environment variable ([muffinista](https://github.com/mastodon/mastodon/pull/19589)) -- Add `DB_POOL` environment variable support for streaming server ([Gargron](https://github.com/mastodon/mastodon/pull/23470)) -- Add `MIN_THREADS` environment variable to set minimum Puma threads ([jimeh](https://github.com/mastodon/mastodon/pull/21048)) -- Add explanation text to log-in page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20946)) -- Add user profile OpenGraph tag on post pages ([bramus](https://github.com/mastodon/mastodon/pull/21423)) -- Add maskable icon support for Android ([workeffortwaste](https://github.com/mastodon/mastodon/pull/20904)) -- Add Belarusian to supported languages ([Mixaill](https://github.com/mastodon/mastodon/pull/22022)) -- Add Western Frisian to supported languages ([ykzts](https://github.com/mastodon/mastodon/pull/18602)) -- Add Montenegrin to the language picker ([ayefries](https://github.com/mastodon/mastodon/pull/21013)) -- Add Southern Sami and Lule Sami to the language picker ([Jullan-M](https://github.com/mastodon/mastodon/pull/21262)) -- Add logging for Rails cache timeouts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21667)) -- Add color highlight for active hashtag “follow” button ([MFTabriz](https://github.com/mastodon/mastodon/pull/21629)) -- Add brotli compression to `assets:precompile` ([Izorkin](https://github.com/mastodon/mastodon/pull/19025)) -- Add “disabled” account filter to the `/admin/accounts` UI ([tribela](https://github.com/mastodon/mastodon/pull/21282)) -- Add transparency to modal background for accessibility ([edent](https://github.com/mastodon/mastodon/pull/18081)) -- Add `lang` attribute to image description textarea and poll option field ([c960657](https://github.com/mastodon/mastodon/pull/23293)) -- Add `spellcheck` attribute to Content Warning and poll option input fields ([c960657](https://github.com/mastodon/mastodon/pull/23395)) -- Add `title` attribute to video elements in media attachments ([bramus](https://github.com/mastodon/mastodon/pull/21420)) -- Add left and right margins to emojis ([dsblank](https://github.com/mastodon/mastodon/pull/20464)) -- Add `roles` attribute to `Account` entities in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23255), [tribela](https://github.com/mastodon/mastodon/pull/23428)) -- Add `reading:autoplay:gifs` to `/api/v1/preferences` ([j-f1](https://github.com/mastodon/mastodon/pull/22706)) -- Add `hide_collections` parameter to `/api/v1/accounts/credentials` ([CarlSchwan](https://github.com/mastodon/mastodon/pull/22790)) -- Add `policy` attribute to web push subscription objects in REST API at `/api/v1/push/subscriptions` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23210)) -- Add metrics endpoint to streaming API ([Gargron](https://github.com/mastodon/mastodon/pull/23388), [Gargron](https://github.com/mastodon/mastodon/pull/23469)) -- Add more specific error messages to HTTP signature verification ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21617)) -- Add Storj DCS to cloud object storage options in the `mastodon:setup` rake task ([jtolio](https://github.com/mastodon/mastodon/pull/21929)) -- Add checkmark symbol in the checkbox for sensitive media ([sidp](https://github.com/mastodon/mastodon/pull/22795)) -- Add missing accessibility attributes to logout link in modals ([kytta](https://github.com/mastodon/mastodon/pull/22549)) -- Add missing accessibility attributes to “Hide image” button in `MediaGallery` ([hs4man21](https://github.com/mastodon/mastodon/pull/22513)) -- Add missing accessibility attributes to hide content warning field when disabled ([hs4man21](https://github.com/mastodon/mastodon/pull/22568)) -- Add `aria-hidden` to footer circle dividers to improve accessibility ([hs4man21](https://github.com/mastodon/mastodon/pull/22576)) -- Add `lang` attribute to compose form inputs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23240)) - -### Changed - -- **Ensure exact match is the first result in hashtag searches** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21315)) -- Change account search to return followed accounts first ([dariusk](https://github.com/mastodon/mastodon/pull/22956)) -- Change batch account suspension to create a strike ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20897)) -- Change default reply language to match the default language when replying to a translated post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22272)) -- Change misleading wording about waitlists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20850)) -- Increase width of the unread notification border ([connorshea](https://github.com/mastodon/mastodon/pull/21692)) -- Change new post notification button on profiles to make it more apparent when it is enabled ([tribela](https://github.com/mastodon/mastodon/pull/22541)) -- Change trending tags admin interface to always show batch action controls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23013)) -- Change wording of some OAuth scope descriptions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22491)) -- Change wording of admin report handling actions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18388)) -- Change confirm prompts for relationships management ([tribela](https://github.com/mastodon/mastodon/pull/19411)) -- Change language surrounding disability in prompts for media descriptions ([hs4man21](https://github.com/mastodon/mastodon/pull/20923)) -- Change confusing wording in the sign in banner ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22490)) -- Change `POST /settings/applications/:id` to regenerate token on scopes change ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23359)) -- Change account moderation notes to make links clickable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22553)) -- Change link previews for statuses to never use avatar as fallback ([Gargron](https://github.com/mastodon/mastodon/pull/23376)) -- Change email address input to be read-only for logged-in users when requesting a new confirmation e-mail ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23247)) -- Change notifications per page from 15 to 40 in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/23348)) -- Change number of stored items in home feed from 400 to 800 ([Gargron](https://github.com/mastodon/mastodon/pull/23349)) -- Change API rate limits from 300/5min per user to 1500/5min per user, 300/5min per app ([Gargron](https://github.com/mastodon/mastodon/pull/23347)) -- Save avatar or header correctly even if the other one fails ([tribela](https://github.com/mastodon/mastodon/pull/18465)) -- Change `referrer-policy` to `same-origin` application-wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23014), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23037)) -- Add 'private' to `Cache-Control`, match Rails expectations ([daxtens](https://github.com/mastodon/mastodon/pull/20608)) -- Make the button that expands the compose form differentiable from the button that publishes a post ([Tak](https://github.com/mastodon/mastodon/pull/20864)) -- Change automatic post deletion configuration to be accessible to moved users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20774)) -- Make tag following idempotent ([trwnh](https://github.com/mastodon/mastodon/pull/20860), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21285)) -- Use buildx functions for faster builds ([inductor](https://github.com/mastodon/mastodon/pull/20692)) -- Split off Dockerfile components for faster builds ([moritzheiber](https://github.com/mastodon/mastodon/pull/20933), [ineffyble](https://github.com/mastodon/mastodon/pull/20948), [BtbN](https://github.com/mastodon/mastodon/pull/21028)) -- Change last occurrence of “silence” to “limit” in UI text ([cincodenada](https://github.com/mastodon/mastodon/pull/20637)) -- Change “hide toot” to “hide post” ([seanthegeek](https://github.com/mastodon/mastodon/pull/22385)) -- Don't allow URLs that contain non-normalized paths to be verified ([dgl](https://github.com/mastodon/mastodon/pull/20999)) -- Change the “Trending now” header to be a link to the Explore page ([connorshea](https://github.com/mastodon/mastodon/pull/21759)) -- Change PostgreSQL connection timeout from 2 minutes to 15 seconds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21790)) -- Make handle more easily selectable on profile page ([cadars](https://github.com/mastodon/mastodon/pull/21479)) -- Allow admins to refresh remotely-suspended accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22327)) -- Change dropdown menu to contain “Copy link to post” even for non-public posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21316)) -- Allow adding relays in secure mode and limited federation mode ([ineffyble](https://github.com/mastodon/mastodon/pull/22324)) -- Change timestamps to be displayed using the user's timezone throughout the moderation interface ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/21878), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22555)) -- Change CSP directives on API to be tight and concise ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20960)) -- Change web UI to not autofocus the compose form ([raboof](https://github.com/mastodon/mastodon/pull/16517), [Akkiesoft](https://github.com/mastodon/mastodon/pull/23094)) -- Change idempotency key handling for posting when database access is slow ([lambda](https://github.com/mastodon/mastodon/pull/21840)) -- Change remote media files to be downloaded outside of transactions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21796)) -- Improve contrast of charts in “poll has ended” notifications ([j-f1](https://github.com/mastodon/mastodon/pull/22575)) -- Change OEmbed detection and validation to be somewhat more lenient ([ineffyble](https://github.com/mastodon/mastodon/pull/22533)) -- Widen ElasticSearch version detection to not display a warning for OpenSearch ([VyrCossont](https://github.com/mastodon/mastodon/pull/22422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23064)) -- Change link verification to allow pages larger than 1MB as long as the link is in the first 1MB ([untitaker](https://github.com/mastodon/mastodon/pull/22879)) -- Update default Node.js version to Node.js 16 ([ineffyble](https://github.com/mastodon/mastodon/pull/22223), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22342)) - -### Removed - -- Officially remove support for Ruby 2.6 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21477)) -- Remove `object-fit` polyfill used for old versions of Microsoft Edge ([shuuji3](https://github.com/mastodon/mastodon/pull/22693)) -- Remove `intersection-observer` polyfill for old Safari support ([shuuji3](https://github.com/mastodon/mastodon/pull/23284)) -- Remove empty `title` tag from mailer layout ([nametoolong](https://github.com/mastodon/mastodon/pull/23078)) -- Remove post count and last posts from ActivityPub representation of hashtag collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23460)) - -### Fixed - -- **Fix changing domain block severity not undoing individual account effects** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22135)) -- Fix suspension worker crashing on S3-compatible setups without ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22487)) -- Fix possible race conditions when suspending/unsuspending accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22363)) -- Fix being stuck in edit mode when deleting the edited posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22126)) -- Fix attached media uploads not being cleared when replying to a post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23504)) -- Fix filters not being applied to some notification types ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23211)) -- Fix incorrect link in push notifications for some event types ([elizabeth-dev](https://github.com/mastodon/mastodon/pull/23286)) -- Fix some performance issues with `/admin/instances` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21907)) -- Fix some pre-4.0 admin audit logs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22091)) -- Fix moderation audit log items for warnings having incorrect links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23242)) -- Fix account activation being sometimes triggered before email confirmation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23245)) -- Fix missing OAuth scopes for admin APIs ([trwnh](https://github.com/mastodon/mastodon/pull/20918), [trwnh](https://github.com/mastodon/mastodon/pull/20979)) -- Fix voter count not being cleared when a poll is reset ([afontenot](https://github.com/mastodon/mastodon/pull/21700)) -- Fix attachments of edited posts not being fetched ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21565)) -- Fix irreversible and whole_word parameters handling in `/api/v1/filters` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21988)) -- Fix 500 error when marking posts as sensitive while some of them are deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22134)) -- Fix expanded posts not always being scrolled into view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21797)) -- Fix not being able to scroll the remote interaction modal on small screens ([xendke](https://github.com/mastodon/mastodon/pull/21763)) -- Fix not being able to scroll in post history modal ([cadars](https://github.com/mastodon/mastodon/pull/23396)) -- Fix audio player volume control on Safari ([minacle](https://github.com/mastodon/mastodon/pull/23187)) -- Fix disappearing “Explore” tabs on Safari ([nyura](https://github.com/mastodon/mastodon/pull/20917), [ykzts](https://github.com/mastodon/mastodon/pull/20982)) -- Fix wrong padding in RTL layout ([Gargron](https://github.com/mastodon/mastodon/pull/23157)) -- Fix drag & drop upload area display in single-column mode ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23217)) -- Fix being unable to get a single EmailDomainBlock from the admin API ([trwnh](https://github.com/mastodon/mastodon/pull/20846)) -- Fix admin-set follow recommandations being case-sensitive ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23500)) -- Fix unserialized `role` on account entities in admin API ([Gargron](https://github.com/mastodon/mastodon/pull/23290)) -- Fix pagination of followed tags ([trwnh](https://github.com/mastodon/mastodon/pull/20861)) -- Fix dropdown menu positions when scrolling ([sidp](https://github.com/mastodon/mastodon/pull/22916), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23062)) -- Fix email with empty domain name labels passing validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23246)) -- Fix mysterious registration failure when “Require a reason to join” is set with open registrations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22127)) -- Fix attachment rendering of edited posts in OpenGraph ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22270)) -- Fix invalid/empty RSS feed link on account pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20772)) -- Fix error in `VerifyLinkService` when processing links with no href ([joshuap](https://github.com/mastodon/mastodon/pull/20741)) -- Fix error in `VerifyLinkService` when processing links with invalid URLs ([untitaker](https://github.com/mastodon/mastodon/pull/23204)) -- Fix media uploads with FFmpeg 5 ([dead10ck](https://github.com/mastodon/mastodon/pull/21191)) -- Fix sensitive flag not being set when replying to a post with a content warning under certain conditions ([kedamaDQ](https://github.com/mastodon/mastodon/pull/21724)) -- Fix misleading message briefly showing up when loading follow requests under some conditions ([c960657](https://github.com/mastodon/mastodon/pull/23386)) -- Fix “Share @:user's profile” profile menu item not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21490)) -- Fix crash and incorrect behavior in `tootctl domains crawl` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19004)) -- Fix autoplay on iOS ([jamesadney](https://github.com/mastodon/mastodon/pull/21422)) -- Fix user clean-up scheduler crash when an unconfirmed account has a moderation note ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23318)) -- Fix spaces not being stripped in admin account search ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21324)) -- Fix spaces not being stripped when adding relays ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22655)) -- Fix infinite loading spinner instead of soft 404 for non-existing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21303)) -- Fix minor visual issue with the top border of verified account fields ([j-f1](https://github.com/mastodon/mastodon/pull/22006)) -- Fix pending account approval and rejection not being recorded in the admin audit log ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/22088)) -- Fix “Sign up” button with closed registrations not opening modal on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22060)) -- Fix UI header overflowing on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21783)) -- Fix 500 error when trying to migrate to an invalid address ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21462)) -- Fix crash when trying to fetch unobtainable avatar of user using external authentication ([lochiiconnectivity](https://github.com/mastodon/mastodon/pull/22462)) -- Fix processing error on incoming malformed JSON-LD under some situations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23416)) -- Fix potential duplicate posts in Explore tab ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22121)) -- Fix deprecation warning in `tootctl accounts rotate` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22120)) -- Fix styling of featured tags in light theme ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23252)) -- Fix missing style in warning and strike cards ([AtelierSnek](https://github.com/mastodon/mastodon/pull/22177), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22302)) -- Fix wasteful request to `/api/v1/custom_emojis` when not logged in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22326)) -- Fix replies sometimes being delivered to user-blocked domains ([tribela](https://github.com/mastodon/mastodon/pull/22117)) -- Fix admin dashboard crash when using some ElasticSearch replacements ([cortices](https://github.com/mastodon/mastodon/pull/21006)) -- Fix profile avatar being slightly offset into left border ([RiedleroD](https://github.com/mastodon/mastodon/pull/20994)) -- Fix N+1 queries in `NotificationsController` ([nametoolong](https://github.com/mastodon/mastodon/pull/21202)) -- Fix being unable to react to announcements with the keycap number sign emoji ([kescherCode](https://github.com/mastodon/mastodon/pull/22231)) -- Fix height computation of post embeds ([hodgesmr](https://github.com/mastodon/mastodon/pull/22141)) -- Fix accessibility issue of the search bar due to hidden placeholder ([alexstine](https://github.com/mastodon/mastodon/pull/21275)) -- Fix layout change handler not being removed due to a typo ([nschonni](https://github.com/mastodon/mastodon/pull/21829)) -- Fix typo in the default `S3_HOSTNAME` used in the `mastodon:setup` rake task ([danp](https://github.com/mastodon/mastodon/pull/19932)) -- Fix the top action bar appearing in the multi-column layout ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20943)) -- Fix inability to use local LibreTranslate without setting `ALLOWED_PRIVATE_ADDRESSES` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21926)) -- Fix punycoded local domains not being prettified in initial state ([Tritlo](https://github.com/mastodon/mastodon/pull/21440)) -- Fix CSP violation warning by removing inline CSS from SVG logo ([luxiaba](https://github.com/mastodon/mastodon/pull/20814)) -- Fix margin for search field on medium window size ([minacle](https://github.com/mastodon/mastodon/pull/21606)) -- Fix search popout scrolling with the page in single-column mode ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/16463)) -- Fix minor post cache hydration discrepancy ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19879)) -- Fix `・` detection in hashtags ([parthoghosh24](https://github.com/mastodon/mastodon/pull/22888)) -- Fix hashtag follows bypassing user blocks ([tribela](https://github.com/mastodon/mastodon/pull/22849)) -- Fix moved accounts being incorrectly redirected to account settings when trying to view a remote profile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22497)) -- Fix site upload validations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22479)) -- Fix “Add new domain block” button using last submitted search value instead of the current one ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22485)) -- Fix misleading hashtag warning when posting with “Followers only” or “Mentioned people only” visibility ([n0toose](https://github.com/mastodon/mastodon/pull/22827)) -- Fix embedded posts with videos grabbing focus ([Akkiesoft](https://github.com/mastodon/mastodon/pull/22778)) -- Fix `$` not being escaped in `.env.production` files generated by the `mastodon:setup` rake task ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23012), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23072)) -- Fix sanitizer parsing link text as HTML when stripping unsupported links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22558)) -- Fix `scheduled_at` input not using `datetime-local` when editing announcements ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21896)) -- Fix REST API serializer for `Account` not including `moved` when the moved account has itself moved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22483)) -- Fix `/api/v1/admin/trends/tags` using wrong serializer ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18943)) -- Fix situations in which instance actor can be set to a Mastodon-incompatible name ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22307)) - -### Security - -- Add `form-action` CSP directive ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20781), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20958), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20962)) -- Fix unbounded recursion in account discovery ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22025)) -- Revoke all authorized applications on password reset ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/21325)) -- Fix unbounded recursion in post discovery ([ClearlyClaire,nametoolong](https://github.com/mastodon/mastodon/pull/23506)) - -## [4.0.2] - 2022-11-15 - -### Fixed - -- Fix wrong color on mentions hidden behind content warning in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/20724)) -- Fix filters from other users being used in the streaming service ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20719)) -- Fix `unsafe-eval` being used when `wasm-unsafe-eval` is enough in Content Security Policy ([Gargron](https://github.com/mastodon/mastodon/pull/20729), [prplecake](https://github.com/mastodon/mastodon/pull/20606)) - -## [4.0.1] - 2022-11-14 - -### Fixed - -- Fix nodes order being sometimes mangled when rewriting emoji ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20677)) - -## [4.0.0] - 2022-11-14 - -Some of the features in this release have been funded through the [NGI0 Discovery](https://nlnet.nl/discovery) Fund, a fund established by [NLnet](https://nlnet.nl/) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825322. - -### Added - -- Add ability to filter followed accounts' posts by language ([Gargron](https://github.com/mastodon/mastodon/pull/19095), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19268)) -- **Add ability to follow hashtags** ([Gargron](https://github.com/mastodon/mastodon/pull/18809), [Gargron](https://github.com/mastodon/mastodon/pull/18862), [Gargron](https://github.com/mastodon/mastodon/pull/19472), [noellabo](https://github.com/mastodon/mastodon/pull/18924)) -- Add ability to filter individual posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18945)) -- **Add ability to translate posts** ([Gargron](https://github.com/mastodon/mastodon/pull/19218), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19433), [Gargron](https://github.com/mastodon/mastodon/pull/19453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19434), [Gargron](https://github.com/mastodon/mastodon/pull/19388), [ykzts](https://github.com/mastodon/mastodon/pull/19244), [Gargron](https://github.com/mastodon/mastodon/pull/19245)) -- Add featured tags to web UI ([noellabo](https://github.com/mastodon/mastodon/pull/19408), [noellabo](https://github.com/mastodon/mastodon/pull/19380), [noellabo](https://github.com/mastodon/mastodon/pull/19358), [noellabo](https://github.com/mastodon/mastodon/pull/19409), [Gargron](https://github.com/mastodon/mastodon/pull/19382), [ykzts](https://github.com/mastodon/mastodon/pull/19418), [noellabo](https://github.com/mastodon/mastodon/pull/19403), [noellabo](https://github.com/mastodon/mastodon/pull/19404), [Gargron](https://github.com/mastodon/mastodon/pull/19398), [Gargron](https://github.com/mastodon/mastodon/pull/19712), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20018)) -- **Add support for language preferences for trending statuses and links** ([Gargron](https://github.com/mastodon/mastodon/pull/18288), [Gargron](https://github.com/mastodon/mastodon/pull/19349), [ykzts](https://github.com/mastodon/mastodon/pull/19335)) - - Previously, you could only see trends in your current language - - For less popular languages, that meant empty trends - - Now, trends in your preferred languages' are shown on top, with others beneath -- Add server rules to sign-up flow ([Gargron](https://github.com/mastodon/mastodon/pull/19296)) -- Add privacy icons to report modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19190)) -- Add `noopener` to links to remote profiles in web UI ([shleeable](https://github.com/mastodon/mastodon/pull/19014)) -- Add option to open original page in dropdowns of remote content in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/20299)) -- Add warning for sensitive audio posts in web UI ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/17885)) -- Add language attribute to posts in web UI ([tribela](https://github.com/mastodon/mastodon/pull/18544)) -- Add support for uploading WebP files ([Saiv46](https://github.com/mastodon/mastodon/pull/18506)) -- Add support for uploading `audio/vnd.wave` files ([tribela](https://github.com/mastodon/mastodon/pull/18737)) -- Add support for uploading AVIF files ([txt-file](https://github.com/mastodon/mastodon/pull/19647)) -- Add support for uploading HEIC files ([Gargron](https://github.com/mastodon/mastodon/pull/19618)) -- Add more debug information when processing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19209)) -- **Add retention policy for cached content and media** ([Gargron](https://github.com/mastodon/mastodon/pull/19232), [zunda](https://github.com/mastodon/mastodon/pull/19478), [Gargron](https://github.com/mastodon/mastodon/pull/19458), [Gargron](https://github.com/mastodon/mastodon/pull/19248)) - - Set for how long remote posts or media should be cached on your server - - Hands-off alternative to `tootctl` commands -- **Add customizable user roles** ([Gargron](https://github.com/mastodon/mastodon/pull/18641), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18812), [Gargron](https://github.com/mastodon/mastodon/pull/19040), [tribela](https://github.com/mastodon/mastodon/pull/18825), [tribela](https://github.com/mastodon/mastodon/pull/18826), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18776), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18777), [unextro](https://github.com/mastodon/mastodon/pull/18786), [tribela](https://github.com/mastodon/mastodon/pull/18824), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19436)) - - Previously, there were 3 hard-coded roles, user, moderator, and admin - - Create your own roles and decide which permissions they should have -- Add notifications for new reports ([Gargron](https://github.com/mastodon/mastodon/pull/18697), [Gargron](https://github.com/mastodon/mastodon/pull/19475)) -- Add ability to select all accounts matching search for batch actions in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19053), [Gargron](https://github.com/mastodon/mastodon/pull/19054)) -- Add ability to view previous edits of a status in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19462)) -- Add ability to block sign-ups from IP ([Gargron](https://github.com/mastodon/mastodon/pull/19037)) -- **Add webhooks to admin UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18510)) -- Add admin API for managing domain allows ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18668)) -- Add admin API for managing domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18247)) -- Add admin API for managing e-mail domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19066)) -- Add admin API for managing canonical e-mail blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19067)) -- Add admin API for managing IP blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19065), [trwnh](https://github.com/mastodon/mastodon/pull/20207)) -- Add `sensitized` attribute to accounts in admin REST API ([trwnh](https://github.com/mastodon/mastodon/pull/20094)) -- Add `services` and `metadata` to the NodeInfo endpoint ([MFTabriz](https://github.com/mastodon/mastodon/pull/18563)) -- Add `--remove-role` option to `tootctl accounts modify` ([Gargron](https://github.com/mastodon/mastodon/pull/19477)) -- Add `--days` option to `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/18425)) -- Add `EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18642)) -- Add `IP_RETENTION_PERIOD` and `SESSION_RETENTION_PERIOD` environment variables ([kescherCode](https://github.com/mastodon/mastodon/pull/18757)) -- Add `http_hidden_proxy` environment variable ([tribela](https://github.com/mastodon/mastodon/pull/18427)) -- Add `ENABLE_STARTTLS` environment variable ([erbridge](https://github.com/mastodon/mastodon/pull/20321)) -- Add caching for payload serialization during fan-out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19637), [Gargron](https://github.com/mastodon/mastodon/pull/19642), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19746), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19747), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19963)) -- Add assets from Twemoji 14.0 ([Gargron](https://github.com/mastodon/mastodon/pull/19733)) -- Add reputation and followers score boost to SQL-only account search ([Gargron](https://github.com/mastodon/mastodon/pull/19251)) -- Add Scots, Balaibalan, Láadan, Lingua Franca Nova, Lojban, Toki Pona to languages list ([VyrCossont](https://github.com/mastodon/mastodon/pull/20168)) -- Set autocomplete hints for e-mail, password and OTP fields ([rcombs](https://github.com/mastodon/mastodon/pull/19833), [offbyone](https://github.com/mastodon/mastodon/pull/19946), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20071)) -- Add support for DigitalOcean Spaces in setup wizard ([v-aisac](https://github.com/mastodon/mastodon/pull/20573)) - -### Changed - -- **Change brand color and logotypes** ([Gargron](https://github.com/mastodon/mastodon/pull/18592), [Gargron](https://github.com/mastodon/mastodon/pull/18639), [Gargron](https://github.com/mastodon/mastodon/pull/18691), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18634), [Gargron](https://github.com/mastodon/mastodon/pull/19254), [mayaeh](https://github.com/mastodon/mastodon/pull/18710)) -- **Change post editing to be enabled in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/19103)) -- **Change web UI to work for logged-out users** ([Gargron](https://github.com/mastodon/mastodon/pull/18961), [Gargron](https://github.com/mastodon/mastodon/pull/19250), [Gargron](https://github.com/mastodon/mastodon/pull/19294), [Gargron](https://github.com/mastodon/mastodon/pull/19306), [Gargron](https://github.com/mastodon/mastodon/pull/19315), [ykzts](https://github.com/mastodon/mastodon/pull/19322), [Gargron](https://github.com/mastodon/mastodon/pull/19412), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19437), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19415), [Gargron](https://github.com/mastodon/mastodon/pull/19348), [Gargron](https://github.com/mastodon/mastodon/pull/19295), [Gargron](https://github.com/mastodon/mastodon/pull/19422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19414), [Gargron](https://github.com/mastodon/mastodon/pull/19319), [Gargron](https://github.com/mastodon/mastodon/pull/19345), [Gargron](https://github.com/mastodon/mastodon/pull/19310), [Gargron](https://github.com/mastodon/mastodon/pull/19301), [Gargron](https://github.com/mastodon/mastodon/pull/19423), [ykzts](https://github.com/mastodon/mastodon/pull/19471), [ykzts](https://github.com/mastodon/mastodon/pull/19333), [ykzts](https://github.com/mastodon/mastodon/pull/19337), [ykzts](https://github.com/mastodon/mastodon/pull/19272), [ykzts](https://github.com/mastodon/mastodon/pull/19468), [Gargron](https://github.com/mastodon/mastodon/pull/19466), [Gargron](https://github.com/mastodon/mastodon/pull/19457), [Gargron](https://github.com/mastodon/mastodon/pull/19426), [Gargron](https://github.com/mastodon/mastodon/pull/19427), [Gargron](https://github.com/mastodon/mastodon/pull/19421), [Gargron](https://github.com/mastodon/mastodon/pull/19417), [Gargron](https://github.com/mastodon/mastodon/pull/19413), [Gargron](https://github.com/mastodon/mastodon/pull/19397), [Gargron](https://github.com/mastodon/mastodon/pull/19387), [Gargron](https://github.com/mastodon/mastodon/pull/19396), [Gargron](https://github.com/mastodon/mastodon/pull/19385), [ykzts](https://github.com/mastodon/mastodon/pull/19334), [ykzts](https://github.com/mastodon/mastodon/pull/19329), [Gargron](https://github.com/mastodon/mastodon/pull/19324), [Gargron](https://github.com/mastodon/mastodon/pull/19318), [Gargron](https://github.com/mastodon/mastodon/pull/19316), [Gargron](https://github.com/mastodon/mastodon/pull/19263), [trwnh](https://github.com/mastodon/mastodon/pull/19305), [ykzts](https://github.com/mastodon/mastodon/pull/19273), [Gargron](https://github.com/mastodon/mastodon/pull/19801), [Gargron](https://github.com/mastodon/mastodon/pull/19790), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19773), [Gargron](https://github.com/mastodon/mastodon/pull/19798), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19724), [Gargron](https://github.com/mastodon/mastodon/pull/19709), [Gargron](https://github.com/mastodon/mastodon/pull/19514), [Gargron](https://github.com/mastodon/mastodon/pull/19562), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19981), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19978), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20148), [Gargron](https://github.com/mastodon/mastodon/pull/20302), [cutls](https://github.com/mastodon/mastodon/pull/20400)) - - The web app can now be accessed without being logged in - - No more `/web` prefix on web app paths - - Profiles, posts, and other public pages now use the same interface for logged in and logged out users - - The web app displays a server information banner - - Pop-up windows for remote interaction have been replaced with a modal window - - No need to type in your username for remote interaction, copy-paste-to-search method explained - - Various hints throughout the app explain what the different timelines are - - New about page design - - New privacy policy page design shows when the policy was last updated - - All sections of the web app now have appropriate window titles - - The layout of the interface has been streamlined between different screen sizes - - Posts now use more horizontal space -- Change label of publish button to be "Publish" again in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18583)) -- Change language to be carried over on reply in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18557)) -- Change "Unfollow" to "Cancel follow request" when request still pending in web UI ([prplecake](https://github.com/mastodon/mastodon/pull/19363)) -- **Change post filtering system** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18058), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19050), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18894), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19051), [noellabo](https://github.com/mastodon/mastodon/pull/18923), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18744), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19878), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20567)) - - Filtered keywords and phrases can now be grouped into named categories - - Filtered posts show which exact filter was hit - - Individual posts can be added to a filter - - You can peek inside filtered posts anyway -- Change path of privacy policy page from `/terms` to `/privacy-policy` ([Gargron](https://github.com/mastodon/mastodon/pull/19249)) -- Change how hashtags are normalized ([Gargron](https://github.com/mastodon/mastodon/pull/18795), [Gargron](https://github.com/mastodon/mastodon/pull/18863), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18854)) -- Change settings area to be separated into categories in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19407), [Gargron](https://github.com/mastodon/mastodon/pull/19533)) -- Change "No accounts selected" errors to use the appropriate noun in admin UI ([prplecake](https://github.com/mastodon/mastodon/pull/19356)) -- Change e-mail domain blocks to match subdomains of blocked domains ([Gargron](https://github.com/mastodon/mastodon/pull/18979)) -- Change custom emoji file size limit from 50 KB to 256 KB ([Gargron](https://github.com/mastodon/mastodon/pull/18788)) -- Change "Allow trends without prior review" setting to also work for trending posts ([Gargron](https://github.com/mastodon/mastodon/pull/17977)) -- Change admin announcements form to use single inputs for date and time in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18321)) -- Change search API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18963), [Gargron](https://github.com/mastodon/mastodon/pull/19326)) -- Change following and followers API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18964)) -- Change `AUTHORIZED_FETCH` to not block unauthenticated REST API access ([Gargron](https://github.com/mastodon/mastodon/pull/19803)) -- Change Helm configuration ([deepy](https://github.com/mastodon/mastodon/pull/18997), [jgsmith](https://github.com/mastodon/mastodon/pull/18415), [deepy](https://github.com/mastodon/mastodon/pull/18941)) -- Change mentions of blocked users to not be processed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19725)) -- Change max. thumbnail dimensions to 640x360px (360p) ([Gargron](https://github.com/mastodon/mastodon/pull/19619)) -- Change post-processing to be deferred only for large media types ([Gargron](https://github.com/mastodon/mastodon/pull/19617)) -- Change link verification to only work for https links without unicode ([Gargron](https://github.com/mastodon/mastodon/pull/20304), [Gargron](https://github.com/mastodon/mastodon/pull/20295)) -- Change account deletion requests to spread out over time ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20222)) -- Change larger reblogs/favourites numbers to be shortened in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/20303)) -- Change incoming activity processing to happen in `ingress` queue ([Gargron](https://github.com/mastodon/mastodon/pull/20264)) -- Change notifications to not link show preview cards in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20335)) -- Change amount of replies returned for logged out users in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20355)) -- Change in-app links to keep you in-app in web UI ([trwnh](https://github.com/mastodon/mastodon/pull/20540), [Gargron](https://github.com/mastodon/mastodon/pull/20628)) -- Change table header to be sticky in admin UI ([sk22](https://github.com/mastodon/mastodon/pull/20442)) - -### Removed - -- Remove setting that disables account deletes ([Gargron](https://github.com/mastodon/mastodon/pull/17683)) -- Remove digest e-mails ([Gargron](https://github.com/mastodon/mastodon/pull/17985)) -- Remove unnecessary sections from welcome e-mail ([Gargron](https://github.com/mastodon/mastodon/pull/19299)) -- Remove item titles from RSS feeds ([Gargron](https://github.com/mastodon/mastodon/pull/18640)) -- Remove volume number from hashtags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19253)) -- Remove Nanobox configuration ([tonyjiang](https://github.com/mastodon/mastodon/pull/17881)) - -### Fixed - -- Fix rules with same priority being sorted non-deterministically ([Gargron](https://github.com/mastodon/mastodon/pull/20623)) -- Fix error when invalid domain name is submitted ([Gargron](https://github.com/mastodon/mastodon/pull/19474)) -- Fix icons having an image role ([Gargron](https://github.com/mastodon/mastodon/pull/20600)) -- Fix connections to IPv6-only servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20108)) -- Fix unnecessary service worker registration and preloading when logged out in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20341)) -- Fix unnecessary and slow regex construction ([raggi](https://github.com/mastodon/mastodon/pull/20215)) -- Fix `mailers` queue not being used for mailers ([Gargron](https://github.com/mastodon/mastodon/pull/20274)) -- Fix error in webfinger redirect handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20260)) -- Fix report category not being set to `violation` if rule IDs are provided ([trwnh](https://github.com/mastodon/mastodon/pull/20137)) -- Fix nodeinfo metadata attribute being an array instead of an object ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20114)) -- Fix account endorsements not being idempotent ([trwnh](https://github.com/mastodon/mastodon/pull/20118)) -- Fix status and rule IDs not being strings in admin reports REST API ([trwnh](https://github.com/mastodon/mastodon/pull/20122)) -- Fix error on invalid `replies_policy` in REST API ([trwnh](https://github.com/mastodon/mastodon/pull/20126)) -- Fix redrafting a currently-editing post not leaving edit mode in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20023)) -- Fix performance by avoiding method cache busts ([raggi](https://github.com/mastodon/mastodon/pull/19957)) -- Fix opening the language picker scrolling the single-column view to the top in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19983)) -- Fix content warning button missing `aria-expanded` attribute in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19975)) -- Fix redundant `aria-pressed` attributes in web UI ([Brawaru](https://github.com/mastodon/mastodon/pull/19912)) -- Fix crash when external auth provider has no display name set ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19962)) -- Fix followers count not being updated when migrating follows ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19998)) -- Fix double button to clear emoji search input in web UI ([sunny](https://github.com/mastodon/mastodon/pull/19888)) -- Fix missing null check on applications on strike disputes ([kescherCode](https://github.com/mastodon/mastodon/pull/19851)) -- Fix featured tags not saving preferred casing ([Gargron](https://github.com/mastodon/mastodon/pull/19732)) -- Fix language not being saved when editing status ([Gargron](https://github.com/mastodon/mastodon/pull/19543)) -- Fix not being able to input featured tag with hash symbol ([Gargron](https://github.com/mastodon/mastodon/pull/19535)) -- Fix user clean-up scheduler crash when an unconfirmed account has a moderation note ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19629)) -- Fix being unable to withdraw follow request when confirmation modal is disabled in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19687)) -- Fix inaccurate admin log entry for re-sending confirmation e-mails ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19674)) -- Fix edits not being immediately reflected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19673)) -- Fix bookmark import stopping at the first failure ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19669)) -- Fix account action type validation ([Gargron](https://github.com/mastodon/mastodon/pull/19476)) -- Fix upload progress not communicating processing phase in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19530)) -- Fix wrong host being used for custom.css when asset host configured ([Gargron](https://github.com/mastodon/mastodon/pull/19521)) -- Fix account migration form ever using outdated account data ([Gargron](https://github.com/mastodon/mastodon/pull/18429), [nightpool](https://github.com/mastodon/mastodon/pull/19883)) -- Fix error when uploading malformed CSV import ([Gargron](https://github.com/mastodon/mastodon/pull/19509)) -- Fix avatars not using image tags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19488)) -- Fix handling of duplicate and out-of-order notifications in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19693)) -- Fix reblogs being discarded after the reblogged status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19731)) -- Fix indexing scheduler trying to index when Elasticsearch is disabled ([Gargron](https://github.com/mastodon/mastodon/pull/19805)) -- Fix n+1 queries when rendering initial state JSON ([Gargron](https://github.com/mastodon/mastodon/pull/19795)) -- Fix n+1 query during status removal ([Gargron](https://github.com/mastodon/mastodon/pull/19753)) -- Fix OCR not working due to Content Security Policy in web UI ([prplecake](https://github.com/mastodon/mastodon/pull/18817)) -- Fix `nofollow` rel being removed in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19455)) -- Fix language dropdown causing zoom on mobile devices in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19428)) -- Fix button to dismiss suggestions not showing up in search results in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19325)) -- Fix language dropdown sometimes not appearing in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19246)) -- Fix quickly switching notification filters resulting in empty or incorrect list in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19052), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18960)) -- Fix media modal link button in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18877)) -- Fix error upon successful account migration ([Gargron](https://github.com/mastodon/mastodon/pull/19386)) -- Fix negatives values in search index causing queries to fail ([Gargron](https://github.com/mastodon/mastodon/pull/19464), [Gargron](https://github.com/mastodon/mastodon/pull/19481)) -- Fix error when searching for invalid URL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18580)) -- Fix IP blocks not having a unique index ([Gargron](https://github.com/mastodon/mastodon/pull/19456)) -- Fix remote account in contact account setting not being used ([Gargron](https://github.com/mastodon/mastodon/pull/19351)) -- Fix swallowing mentions of unconfirmed/unapproved users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19191)) -- Fix incorrect and slow cache invalidation when blocking domain and removing media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19062)) -- Fix HTTPs redirect behaviour when running as I2P service ([gi-yt](https://github.com/mastodon/mastodon/pull/18929)) -- Fix deleted pinned posts potentially counting towards the pinned posts limit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19005)) -- Fix compatibility with OpenSSL 3.0 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18449)) -- Fix error when a remote report includes a private post the server has no access to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18760)) -- Fix suspicious sign-in mails never being sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18599)) -- Fix fallback locale when somehow user's locale is an empty string ([tribela](https://github.com/mastodon/mastodon/pull/18543)) -- Fix avatar/header not being deleted locally when deleted on remote account ([tribela](https://github.com/mastodon/mastodon/pull/18973)) -- Fix missing `,` in Blurhash validation ([noellabo](https://github.com/mastodon/mastodon/pull/18660)) -- Fix order by most recent not working for relationships page in admin UI ([tribela](https://github.com/mastodon/mastodon/pull/18996)) -- Fix uncaught error when invalid date is supplied to API ([Gargron](https://github.com/mastodon/mastodon/pull/19480)) -- Fix REST API sometimes returning HTML on error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19135)) -- Fix ambiguous column names in `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/19206)) -- Fix ambiguous column names in `tootctl search deploy` ([mashirozx](https://github.com/mastodon/mastodon/pull/18993)) -- Fix `CDN_HOST` not being used in some asset URLs ([tribela](https://github.com/mastodon/mastodon/pull/18662)) -- Fix `CAS_DISPLAY_NAME`, `SAML_DISPLAY_NAME` and `OIDC_DISPLAY_NAME` being ignored ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18568)) -- Fix various typos in comments throughout the codebase ([luzpaz](https://github.com/mastodon/mastodon/pull/18604)) -- Fix CSV import error when rows include unicode characters ([HamptonMakes](https://github.com/mastodon/mastodon/pull/20592)) - -### Security - -- Fix being able to spoof link verification ([Gargron](https://github.com/mastodon/mastodon/pull/20217)) -- Fix emoji substitution not applying only to text nodes in backend code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20641)) -- Fix emoji substitution not applying only to text nodes in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20640)) -- Fix rate limiting for paths with formats ([Gargron](https://github.com/mastodon/mastodon/pull/20675)) -- Fix out-of-bound reads in blurhash transcoder ([delroth](https://github.com/mastodon/mastodon/pull/20388)) - -_For previous changes, review the [stable-3.5 branch](https://github.com/mastodon/mastodon/blob/stable-3.5/CHANGELOG.md)_ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 2ee2e538bc..0000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and unwelcome sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at glitch-abuse@sitedethib.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] - -[homepage]: https://contributor-covenant.org -[version]: https://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 66aa01ffe4..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,96 +0,0 @@ -# Contributing to Mastodon Glitch Edition - -Thank you for your interest in contributing to the `glitch-soc` project! -Here are some guidelines, and ways you can help. - -> (This document is a bit of a work-in-progress, so please bear with us. -> If you don't see what you're looking for here, please don't hesitate to reach out!) - -## Translations - -You can submit glitch-soc-specific translations via [Crowdin](https://crowdin.com/project/glitch-soc). They are periodically merged into the codebase. - -[![Crowdin](https://badges.crowdin.net/glitch-soc/localized.svg)](https://crowdin.com/project/glitch-soc) - -## Planning - -Right now a lot of the planning for this project takes place in our development Discord, or through GitHub Issues and Projects. -We're working on ways to improve the planning structure and better solicit feedback, and if you feel like you can help in this respect, feel free to give us a holler. - -## Documentation - -The documentation for this repository is available at [`glitch-soc/docs`](https://github.com/glitch-soc/docs) (online at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/)). -Right now, we've mostly focused on the features that make this fork different from upstream in some manner. -Adding screenshots, improving descriptions, and so forth are all ways to help contribute to the project even if you don't know any code. - -## Frontend Development - -Check out [the documentation here](https://glitch-soc.github.io/docs/contributing/frontend/) for more information. - -## Backend Development - -See the guidelines below. - ---- - -You should also try to follow the guidelines set out in the original `CONTRIBUTING.md` from `mastodon/mastodon`, reproduced below. - -
- -# Contributing - -Thank you for considering contributing to Mastodon 🐘 - -You can contribute in the following ways: - -- Finding and reporting bugs -- Translating the Mastodon interface into various languages -- Contributing code to Mastodon by fixing bugs or implementing features -- Improving the documentation - -If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon). - -Please review the org-level [contribution guidelines] for high-level acceptance -criteria guidance. - -[contribution guidelines]: https://github.com/mastodon/.github/blob/main/CONTRIBUTING.md - -## API Changes and Additions - -Please note that any changes or additions made to the API should have an accompanying pull request on [our documentation repository](https://github.com/mastodon/documentation). - -## Bug reports - -Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/mastodon/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected. - -## Translations - -You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). They are periodically merged into the codebase. - -[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)](https://crowdin.com/project/mastodon) - -## Pull requests - -**Please use clean, concise titles for your pull requests.** Unless the pull request is about refactoring code, updating dependencies or other internal tasks, assume that the person reading the pull request title is not a programmer or Mastodon developer, but instead a Mastodon user or server administrator, and **try to describe your change or fix from their perspective**. We use commit squashing, so the final commit in the main branch will carry the title of the pull request, and commits from the main branch are fed into the changelog. The changelog is separated into [keepachangelog.com categories](https://keepachangelog.com/en/1.0.0/), and while that spec does not prescribe how the entries ought to be named, for easier sorting, start your pull request titles using one of the verbs "Add", "Change", "Deprecate", "Remove", or "Fix" (present tense). - -Example: - -| Not ideal | Better | -| ------------------------------------ | ------------------------------------------------------------- | -| Fixed NoMethodError in RemovalWorker | Fix nil error when removing statuses caused by race condition | - -It is not always possible to phrase every change in such a manner, but it is desired. - -**The smaller the set of changes in the pull request is, the quicker it can be reviewed and merged.** Splitting tasks into multiple smaller pull requests is often preferable. - -**Pull requests that do not pass automated checks may not be reviewed**. In particular, you need to keep in mind: - -- Unit and integration tests (rspec, jest) -- Code style rules (rubocop, eslint) -- Normalization of locale files (i18n-tasks) - -## Documentation - -The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation). - -
diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index bc7cd3b682..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,404 +0,0 @@ -# syntax=docker/dockerfile:1.9 - -# This file is designed for production server deployment, not local development work -# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/README.md#docker - -# Please see https://docs.docker.com/engine/reference/builder for information about -# the extended buildx capabilities used in this file. -# Make sure multiarch TARGETPLATFORM is available for interpolation -# See: https://docs.docker.com/build/building/multi-platform/ -ARG TARGETPLATFORM=${TARGETPLATFORM} -ARG BUILDPLATFORM=${BUILDPLATFORM} - -# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"] -# renovate: datasource=docker depName=docker.io/ruby -ARG RUBY_VERSION="3.3.4" -# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] -# renovate: datasource=node-version depName=node -ARG NODE_MAJOR_VERSION="20" -# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] -ARG DEBIAN_VERSION="bookworm" -# Node image to use for base image based on combined variables (ex: 20-bookworm-slim) -FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node -# Ruby image to use for base image based on combined variables (ex: 3.3.x-slim-bookworm) -FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby - -# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA -# Example: v4.3.0-nightly.2023.11.09+pr-123456 -# Overwrite existence of 'alpha.X' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"] -ARG MASTODON_VERSION_PRERELEASE="" -# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-123456"] -ARG MASTODON_VERSION_METADATA="" - -# Allow Ruby on Rails to serve static files -# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files -ARG RAILS_SERVE_STATIC_FILES="true" -# Allow to use YJIT compiler -# See: https://github.com/ruby/ruby/blob/v3_2_4/doc/yjit/yjit.md -ARG RUBY_YJIT_ENABLE="1" -# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin] -ARG TZ="Etc/UTC" -# Linux UID (user id) for the mastodon user, change with [--build-arg UID=1234] -ARG UID="991" -# Linux GID (group id) for the mastodon user, change with [--build-arg GID=1234] -ARG GID="991" - -# Apply Mastodon build options based on options above -ENV \ -# Apply Mastodon version information - MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \ - MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \ -# Apply Mastodon static files and YJIT options - RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} \ - RUBY_YJIT_ENABLE=${RUBY_YJIT_ENABLE} \ -# Apply timezone - TZ=${TZ} - -ENV \ -# Configure the IP to bind Mastodon to when serving traffic - BIND="0.0.0.0" \ -# Use production settings for Yarn, Node and related nodejs based tools - NODE_ENV="production" \ -# Use production settings for Ruby on Rails - RAILS_ENV="production" \ -# Add Ruby and Mastodon installation to the PATH - DEBIAN_FRONTEND="noninteractive" \ - PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \ -# Optimize jemalloc 5.x performance - MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" \ -# Enable libvips, should not be changed - MASTODON_USE_LIBVIPS=true \ -# Sidekiq will touch tmp/sidekiq_process_has_started_and_will_begin_processing_jobs to indicate it is ready. This can be used for a readiness check in Kubernetes - MASTODON_SIDEKIQ_READY_FILENAME=sidekiq_process_has_started_and_will_begin_processing_jobs - -# Set default shell used for running commands -SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"] - -ARG TARGETPLATFORM - -RUN echo "Target platform is $TARGETPLATFORM" - -RUN \ -# Remove automatic apt cache Docker cleanup scripts - rm -f /etc/apt/apt.conf.d/docker-clean; \ -# Sets timezone - echo "${TZ}" > /etc/localtime; \ -# Creates mastodon user/group and sets home directory - groupadd -g "${GID}" mastodon; \ - useradd -l -u "${UID}" -g "${GID}" -m -d /opt/mastodon mastodon; \ -# Creates /mastodon symlink to /opt/mastodon - ln -s /opt/mastodon /mastodon; - -# Set /opt/mastodon as working directory -WORKDIR /opt/mastodon - -# hadolint ignore=DL3008,DL3005 -RUN \ -# Mount Apt cache and lib directories from Docker buildx caches ---mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ ---mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ -# Apt update & upgrade to check for security updates to Debian image - apt-get update; \ - apt-get dist-upgrade -yq; \ -# Install jemalloc, curl and other necessary components - apt-get install -y --no-install-recommends \ - curl \ - file \ - libjemalloc2 \ - patchelf \ - procps \ - tini \ - tzdata \ - wget \ - ; \ -# Patch Ruby to use jemalloc - patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby; \ -# Discard patchelf after use - apt-get purge -y \ - patchelf \ - ; - -# Create temporary build layer from base image -FROM ruby AS build - -# Copy Node package configuration files into working directory -COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ -COPY .yarn /opt/mastodon/.yarn - -COPY --from=node /usr/local/bin /usr/local/bin -COPY --from=node /usr/local/lib /usr/local/lib - -ARG TARGETPLATFORM - -# hadolint ignore=DL3008 -RUN \ -# Mount Apt cache and lib directories from Docker buildx caches ---mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ ---mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ -# Install build tools and bundler dependencies from APT - apt-get install -y --no-install-recommends \ - autoconf \ - automake \ - build-essential \ - cmake \ - git \ - libgdbm-dev \ - libglib2.0-dev \ - libgmp-dev \ - libicu-dev \ - libidn-dev \ - libpq-dev \ - libssl-dev \ - libtool \ - meson \ - nasm \ - pkg-config \ - shared-mime-info \ - xz-utils \ - # libvips components - libcgif-dev \ - libexif-dev \ - libexpat1-dev \ - libgirepository1.0-dev \ - libheif-dev \ - libimagequant-dev \ - libjpeg62-turbo-dev \ - liblcms2-dev \ - liborc-dev \ - libspng-dev \ - libtiff-dev \ - libwebp-dev \ - # ffmpeg components - libdav1d-dev \ - liblzma-dev \ - libmp3lame-dev \ - libopus-dev \ - libsnappy-dev \ - libvorbis-dev \ - libvpx-dev \ - libx264-dev \ - libx265-dev \ - ; - -RUN \ -# Configure Corepack - rm /usr/local/bin/yarn*; \ - corepack enable; \ - corepack prepare --activate; - -# Create temporary libvips specific build layer from build layer -FROM build AS libvips - -# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"] -# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips -ARG VIPS_VERSION=8.15.2 -# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"] -ARG VIPS_URL=https://github.com/libvips/libvips/releases/download - -WORKDIR /usr/local/libvips/src - -RUN \ - curl -sSL -o vips-${VIPS_VERSION}.tar.xz ${VIPS_URL}/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz; \ - tar xf vips-${VIPS_VERSION}.tar.xz; \ - cd vips-${VIPS_VERSION}; \ - meson setup build --prefix /usr/local/libvips --libdir=lib -Ddeprecated=false -Dintrospection=disabled -Dmodules=disabled -Dexamples=false; \ - cd build; \ - ninja; \ - ninja install; - -# Create temporary ffmpeg specific build layer from build layer -FROM build AS ffmpeg - -# ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"] -# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg -ARG FFMPEG_VERSION=7.0.1 -# ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"] -ARG FFMPEG_URL=https://ffmpeg.org/releases - -WORKDIR /usr/local/ffmpeg/src - -RUN \ - curl -sSL -o ffmpeg-${FFMPEG_VERSION}.tar.xz ${FFMPEG_URL}/ffmpeg-${FFMPEG_VERSION}.tar.xz; \ - tar xf ffmpeg-${FFMPEG_VERSION}.tar.xz; \ - cd ffmpeg-${FFMPEG_VERSION}; \ - ./configure \ - --prefix=/usr/local/ffmpeg \ - --toolchain=hardened \ - --disable-debug \ - --disable-devices \ - --disable-doc \ - --disable-ffplay \ - --disable-network \ - --disable-static \ - --enable-ffmpeg \ - --enable-ffprobe \ - --enable-gpl \ - --enable-libdav1d \ - --enable-libmp3lame \ - --enable-libopus \ - --enable-libsnappy \ - --enable-libvorbis \ - --enable-libvpx \ - --enable-libwebp \ - --enable-libx264 \ - --enable-libx265 \ - --enable-shared \ - --enable-version3 \ - ; \ - make -j$(nproc); \ - make install; - -# Create temporary bundler specific build layer from build layer -FROM build AS bundler - -ARG TARGETPLATFORM - -# Copy Gemfile config into working directory -COPY Gemfile* /opt/mastodon/ - -RUN \ -# Mount Ruby Gem caches ---mount=type=cache,id=gem-cache-${TARGETPLATFORM},target=/usr/local/bundle/cache/,sharing=locked \ -# Configure bundle to prevent changes to Gemfile and Gemfile.lock - bundle config set --global frozen "true"; \ -# Configure bundle to not cache downloaded Gems - bundle config set --global cache_all "false"; \ -# Configure bundle to only process production Gems - bundle config set --local without "development test"; \ -# Configure bundle to not warn about root user - bundle config set silence_root_warning "true"; \ -# Download and install required Gems - bundle install -j"$(nproc)"; - -# Create temporary node specific build layer from build layer -FROM build AS yarn - -ARG TARGETPLATFORM - -# Copy Node package configuration files into working directory -COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ -COPY streaming/package.json /opt/mastodon/streaming/ -COPY .yarn /opt/mastodon/.yarn - -# hadolint ignore=DL3008 -RUN \ ---mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \ ---mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ -# Install Node packages - yarn workspaces focus --production @mastodon/mastodon; - -# Create temporary assets build layer from build layer -FROM build AS precompiler - -# Copy Mastodon sources into precompiler layer -COPY . /opt/mastodon/ - -# Copy bundler and node packages from build layer to container -COPY --from=yarn /opt/mastodon /opt/mastodon/ -COPY --from=bundler /opt/mastodon /opt/mastodon/ -COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ -# Copy libvips components to layer for precompiler -COPY --from=libvips /usr/local/libvips/bin /usr/local/bin -COPY --from=libvips /usr/local/libvips/lib /usr/local/lib - -ARG TARGETPLATFORM - -RUN \ - ldconfig; \ -# Use Ruby on Rails to create Mastodon assets - SECRET_KEY_BASE_DUMMY=1 \ - bundle exec rails assets:precompile; \ -# Cleanup temporary files - rm -fr /opt/mastodon/tmp; - -# Prep final Mastodon Ruby layer -FROM ruby AS mastodon - -ARG TARGETPLATFORM - -# hadolint ignore=DL3008 -RUN \ -# Mount Apt cache and lib directories from Docker buildx caches ---mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ ---mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ -# Mount Corepack and Yarn caches from Docker buildx caches ---mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \ ---mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ -# Apt update install non-dev versions of necessary components - apt-get install -y --no-install-recommends \ - libexpat1 \ - libglib2.0-0 \ - libicu72 \ - libidn12 \ - libpq5 \ - libreadline8 \ - libssl3 \ - libyaml-0-2 \ - # libvips components - libcgif0 \ - libexif12 \ - libheif1 \ - libimagequant0 \ - libjpeg62-turbo \ - liblcms2-2 \ - liborc-0.4-0 \ - libspng0 \ - libtiff6 \ - libwebp7 \ - libwebpdemux2 \ - libwebpmux3 \ - # ffmpeg components - libdav1d6 \ - libmp3lame0 \ - libopencore-amrnb0 \ - libopencore-amrwb0 \ - libopus0 \ - libsnappy1v5 \ - libtheora0 \ - libvorbis0a \ - libvorbisenc2 \ - libvorbisfile3 \ - libvpx7 \ - libx264-164 \ - libx265-199 \ - ; - -# Copy Mastodon sources into final layer -COPY . /opt/mastodon/ - -# Copy compiled assets to layer -COPY --from=precompiler /opt/mastodon/public/packs /opt/mastodon/public/packs -COPY --from=precompiler /opt/mastodon/public/assets /opt/mastodon/public/assets -# Copy bundler components to layer -COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ -# Copy libvips components to layer -COPY --from=libvips /usr/local/libvips/bin /usr/local/bin -COPY --from=libvips /usr/local/libvips/lib /usr/local/lib -# Copy ffpmeg components to layer -COPY --from=ffmpeg /usr/local/ffmpeg/bin /usr/local/bin -COPY --from=ffmpeg /usr/local/ffmpeg/lib /usr/local/lib - -RUN \ - ldconfig; \ -# Smoketest media processors - vips -v; \ - ffmpeg -version; \ - ffprobe -version; - -RUN \ - # Precompile bootsnap code for faster Rails startup - bundle exec bootsnap precompile --gemfile app/ lib/; - -RUN \ -# Pre-create and chown system volume to Mastodon user - mkdir -p /opt/mastodon/public/system; \ - chown mastodon:mastodon /opt/mastodon/public/system; \ -# Set Mastodon user as owner of tmp folder - chown -R mastodon:mastodon /opt/mastodon/tmp; - -# Set the running user for resulting container -USER mastodon -# Expose default Puma ports -EXPOSE 3000 -# Set container tini as default entry point -ENTRYPOINT ["/usr/bin/tini", "--"] diff --git a/FEDERATION.md b/FEDERATION.md deleted file mode 100644 index 2819fa935a..0000000000 --- a/FEDERATION.md +++ /dev/null @@ -1,49 +0,0 @@ -# Federation - -## Supported federation protocols and standards - -- [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server) -- [WebFinger](https://webfinger.net/) -- [Http Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) -- [NodeInfo](https://nodeinfo.diaspora.software/) - -## Supported FEPs - -- [FEP-67ff: FEDERATION.md](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) -- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md) -- [FEP-8fcf: Followers collection synchronization across servers](https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md) -- [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md) - -## ActivityPub in Mastodon - -Mastodon largely follows the ActivityPub server-to-server specification but it makes uses of some non-standard extensions, some of which are required for interacting with Mastodon at all. - -- [Supported ActivityPub vocabulary](https://docs.joinmastodon.org/spec/activitypub/) - -### Required extensions - -#### WebFinger - -In Mastodon, users are identified by a `username` and `domain` pair (e.g., `Gargron@mastodon.social`). -This is used both for discovery and for unambiguously mentioning users across the fediverse. Furthermore, this is part of Mastodon's database design from its very beginnings. - -As a result, Mastodon requires that each ActivityPub actor uniquely maps back to an `acct:` URI that can be resolved via WebFinger. - -- [WebFinger information and examples](https://docs.joinmastodon.org/spec/webfinger/) - -#### HTTP Signatures - -In order to authenticate activities, Mastodon relies on HTTP Signatures, signing every `POST` and `GET` request to other ActivityPub implementations on behalf of the user authoring an activity (for `POST` requests) or an actor representing the Mastodon server itself (for most `GET` requests). - -Mastodon requires all `POST` requests to be signed, and MAY require `GET` requests to be signed, depending on the configuration of the Mastodon server. - -- [HTTP Signatures information and examples](https://docs.joinmastodon.org/spec/security/#http) - -### Optional extensions - -- [Linked-Data Signatures](https://docs.joinmastodon.org/spec/security/#ld) -- [Bearcaps](https://docs.joinmastodon.org/spec/bearcaps/) - -### Additional documentation - -- [Mastodon documentation](https://docs.joinmastodon.org/) diff --git a/Procfile b/Procfile deleted file mode 100644 index d15c835b86..0000000000 --- a/Procfile +++ /dev/null @@ -1,14 +0,0 @@ -web: bin/heroku-web -worker: bundle exec sidekiq - -# For the streaming API, you need a separate app that shares Postgres and Redis: -# -# heroku create -# heroku buildpacks:add heroku/nodejs -# heroku config:set RUN_STREAMING=true -# heroku addons:attach ::DATABASE -# heroku addons:attach ::REDIS -# -# and let the main app use the separate app: -# -# heroku config:set STREAMING_API_BASE_URL=wss://.herokuapp.com -a diff --git a/Procfile.dev b/Procfile.dev deleted file mode 100644 index f81333b04c..0000000000 --- a/Procfile.dev +++ /dev/null @@ -1,4 +0,0 @@ -web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb -sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq -stream: env PORT=4000 yarn workspace @mastodon/streaming start -webpack: bin/webpack-dev-server diff --git a/README.md b/README.md index 5a61fc8af2..5e0103c99e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - add bubble timeline to "Getting started" and "Live feeds" - fix avatar corner rounding to look as intended - add remlit mascot +- slimmed down repository. no need for the backend too. for my tweaked version, you can choose to use an express-hosted version. instead of the below steps, you can just reverse proxy or tunnel localhost:3132 when running frontend-server/server.mjs. diff --git a/Rakefile b/Rakefile deleted file mode 100644 index e51cf0e17e..0000000000 --- a/Rakefile +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require File.expand_path('config/application', __dir__) - -Rails.application.load_tasks diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index 89f5536edc..0000000000 --- a/Vagrantfile +++ /dev/null @@ -1,201 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -ENV["PORT"] ||= "3000" - -$provisionA = <' } - - it 'does not include the HTML in the URL' do - expect(subject).to include '"http://example.com/blahblahblahblah/a"' - end - - it 'does not include a script tag' do - expect(subject).to_not include '' } - - it 'does not include a script tag' do - expect(subject).to_not include '' } - - it 'strips the scripts' do - expect(subject).to_not include '' - end - end - - context 'when given text containing malicious classes' do - let(:text) { 'Show more' } - - it 'strips the malicious classes' do - expect(subject).to_not include 'status__content__spoiler-link' - end - end - - context 'when given text containing ruby tags for east-asian languages' do - let(:text) { '明日 (Ashita)' } - - it 'keeps the ruby tags' do - expect(subject).to eq '明日 (Ashita)' - end - end - end - end -end diff --git a/spec/lib/importer/accounts_index_importer_spec.rb b/spec/lib/importer/accounts_index_importer_spec.rb deleted file mode 100644 index 73f9bce399..0000000000 --- a/spec/lib/importer/accounts_index_importer_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Importer::AccountsIndexImporter do - describe 'import!' do - let(:pool) { Concurrent::FixedThreadPool.new(5) } - let(:importer) { described_class.new(batch_size: 123, executor: pool) } - - before { Fabricate(:account) } - - it 'indexes relevant accounts' do - expect { importer.import! }.to update_index(AccountsIndex) - end - end -end diff --git a/spec/lib/importer/base_importer_spec.rb b/spec/lib/importer/base_importer_spec.rb deleted file mode 100644 index 78e9a869b8..0000000000 --- a/spec/lib/importer/base_importer_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Importer::BaseImporter do - describe 'import!' do - let(:pool) { Concurrent::FixedThreadPool.new(5) } - let(:importer) { described_class.new(batch_size: 123, executor: pool) } - - it 'raises an error' do - expect { importer.import! }.to raise_error(NotImplementedError) - end - end -end diff --git a/spec/lib/importer/public_statuses_index_importer_spec.rb b/spec/lib/importer/public_statuses_index_importer_spec.rb deleted file mode 100644 index bc7c038a97..0000000000 --- a/spec/lib/importer/public_statuses_index_importer_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Importer::PublicStatusesIndexImporter do - describe 'import!' do - let(:pool) { Concurrent::FixedThreadPool.new(5) } - let(:importer) { described_class.new(batch_size: 123, executor: pool) } - - before { Fabricate(:status, account: Fabricate(:account, indexable: true)) } - - it 'indexes relevant statuses' do - expect { importer.import! }.to update_index(PublicStatusesIndex) - end - end -end diff --git a/spec/lib/importer/statuses_index_importer_spec.rb b/spec/lib/importer/statuses_index_importer_spec.rb deleted file mode 100644 index d5e1c9f2cb..0000000000 --- a/spec/lib/importer/statuses_index_importer_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Importer::StatusesIndexImporter do - describe 'import!' do - let(:pool) { Concurrent::FixedThreadPool.new(5) } - let(:importer) { described_class.new(batch_size: 123, executor: pool) } - - before { Fabricate(:status) } - - it 'indexes relevant statuses' do - expect { importer.import! }.to update_index(StatusesIndex) - end - end -end diff --git a/spec/lib/importer/tags_index_importer_spec.rb b/spec/lib/importer/tags_index_importer_spec.rb deleted file mode 100644 index 348990c01e..0000000000 --- a/spec/lib/importer/tags_index_importer_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Importer::TagsIndexImporter do - describe 'import!' do - let(:pool) { Concurrent::FixedThreadPool.new(5) } - let(:importer) { described_class.new(batch_size: 123, executor: pool) } - - before { Fabricate(:tag) } - - it 'indexes relevant tags' do - expect { importer.import! }.to update_index(TagsIndex) - end - end -end diff --git a/spec/lib/link_details_extractor_spec.rb b/spec/lib/link_details_extractor_spec.rb deleted file mode 100644 index b1e5cedced..0000000000 --- a/spec/lib/link_details_extractor_spec.rb +++ /dev/null @@ -1,277 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe LinkDetailsExtractor do - subject { described_class.new(original_url, html, nil) } - - let(:original_url) { 'https://example.com/dog.html?tracking=123' } - - describe '#canonical_url' do - let(:html) { "" } - - context 'when canonical URL points to the same host' do - let(:url) { 'https://example.com/dog.html' } - - it 'ignores the canonical URLs' do - expect(subject.canonical_url).to eq 'https://example.com/dog.html' - end - end - - context 'when canonical URL points to another host' do - let(:url) { 'https://different.example.net/dog.html' } - - it 'ignores the canonical URLs' do - expect(subject.canonical_url).to eq original_url - end - end - - context 'when canonical URL is set to "null"' do - let(:url) { 'null' } - - it 'ignores the canonical URLs' do - expect(subject.canonical_url).to eq original_url - end - end - end - - context 'when only basic metadata is present' do - let(:html) { <<~HTML } - - - - Man bites dog - - - - HTML - - it 'extracts the expected values from html metadata' do - expect(subject) - .to have_attributes( - title: eq('Man bites dog'), - description: eq("A dog's tale"), - language: eq('en') - ) - end - end - - context 'when structured data is present' do - let(:ld_json) do - { - '@context' => 'https://schema.org', - '@type' => 'NewsArticle', - 'headline' => 'Man bites dog', - 'description' => "A dog's tale", - 'datePublished' => '2022-01-31T19:53:00+00:00', - 'author' => { - '@type' => 'Organization', - 'name' => 'Charlie Brown', - }, - 'publisher' => { - '@type' => 'NewsMediaOrganization', - 'name' => 'Pet News', - 'url' => 'https://example.com', - }, - 'inLanguage' => { - name: 'English', - alternateName: 'en', - }, - }.to_json - end - - shared_examples 'structured data' do - it 'extracts the expected values from structured data' do - expect(subject) - .to have_attributes( - title: eq('Man bites dog'), - description: eq("A dog's tale"), - published_at: eq('2022-01-31T19:53:00+00:00'), - author_name: eq('Charlie Brown'), - provider_name: eq('Pet News'), - language: eq('en') - ) - end - end - - context 'when is wrapped in CDATA tags' do - let(:html) { <<~HTML } - - - - - - - HTML - - include_examples 'structured data' - end - - context 'with the first tag is invalid JSON' do - let(:html) { <<~HTML } - - - - - - - - HTML - - include_examples 'structured data' - end - - context 'with the first tag is null' do - let(:html) { <<~HTML } - - - - - - - - HTML - - include_examples 'structured data' - end - - context 'with preceding block of unsupported LD+JSON' do - let(:html) { <<~HTML } - - - - - - - - HTML - - include_examples 'structured data' - end - - context 'with unsupported in same block LD+JSON' do - let(:html) { <<~HTML } - - - - - - - HTML - - include_examples 'structured data' - end - - context 'with author names as array' do - let(:ld_json) do - { - '@context' => 'https://schema.org', - '@type' => 'NewsArticle', - 'headline' => 'A lot of authors', - 'description' => 'But we decided to cram them into one', - 'author' => { - '@type' => 'Person', - 'name' => ['Author 1', 'Author 2'], - }, - }.to_json - end - let(:html) { <<~HTML } - - - - - - - HTML - - it 'joins author names' do - expect(subject.author_name).to eq 'Author 1, Author 2' - end - end - end - - context 'when Open Graph protocol data is present' do - let(:html) { <<~HTML } - - - - - - - - - - - - - - - HTML - - it 'extracts the expected values from open graph data' do - expect(subject) - .to have_attributes( - canonical_url: eq('https://example.com/dog.html'), - title: eq('Man bites dog'), - description: eq("A dog's tale"), - published_at: eq('2022-01-31T19:53:00+00:00'), - author_name: eq('Charlie Brown'), - language: eq('en'), - image: eq('https://example.com/snoopy.jpg'), - image_alt: eq('A good boy'), - provider_name: eq('Pet News') - ) - end - end -end diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb deleted file mode 100644 index 137f85c6ca..0000000000 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ /dev/null @@ -1,1474 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/accounts' - -describe Mastodon::CLI::Accounts do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - # `parallelize_with_progress` cannot run in transactions, so instead, - # stub it with an alternative implementation that runs sequentially - # and can run in transactions. - def stub_parallelize_with_progress! - allow(cli).to receive(:parallelize_with_progress) do |scope, &block| - aggregate = 0 - total = 0 - - scope.reorder(nil).find_each do |record| - value = block.call(record) - aggregate += value if value.is_a?(Integer) - total += 1 - end - - [total, aggregate] - end - end - - describe '#create' do - let(:action) { :create } - - shared_examples 'a new user with given email address and username' do - it 'creates user and accounts from options and displays success message' do - allow(SecureRandom).to receive(:hex).and_return('test_password') - - expect { subject } - .to output_results('OK', 'New password: test_password') - expect(user_from_options).to be_present - expect(account_from_options).to be_present - end - - def user_from_options - User.find_by(email: options[:email]) - end - - def account_from_options - Account.find_local('tootctl_username') - end - end - - context 'when required USERNAME and --email are provided' do - let(:arguments) { ['tootctl_username'] } - - context 'with USERNAME and --email only' do - let(:options) { { email: 'tootctl@example.com' } } - - it_behaves_like 'a new user with given email address and username' - - context 'with invalid --email value' do - let(:options) { { email: 'invalid' } } - - it 'exits with an error message' do - expect { subject } - .to raise_error(Thor::Error, %r{Failure/Error: email}) - end - end - end - - context 'with --confirmed option' do - let(:options) { { email: 'tootctl@example.com', confirmed: true } } - - it_behaves_like 'a new user with given email address and username' - - it 'creates a new user with confirmed status' do - expect { subject } - .to output_results('New password') - - user = User.find_by(email: options[:email]) - - expect(user.confirmed?).to be(true) - end - end - - context 'with --approve option' do - let(:options) { { email: 'tootctl@example.com', approve: true } } - - before do - Form::AdminSettings.new(registrations_mode: 'approved').save - end - - it_behaves_like 'a new user with given email address and username' - - it 'creates a new user with approved status' do - expect { subject } - .to output_results('New password') - - user = User.find_by(email: options[:email]) - - expect(user.approved?).to be(true) - end - end - - context 'with --role option' do - context 'when role exists' do - let(:default_role) { Fabricate(:user_role) } - let(:options) { { email: 'tootctl@example.com', role: default_role.name } } - - it_behaves_like 'a new user with given email address and username' - - it 'creates a new user and assigns the specified role' do - expect { subject } - .to output_results('New password') - - role = User.find_by(email: options[:email])&.role - - expect(role.name).to eq(default_role.name) - end - end - - context 'when role does not exist' do - let(:options) { { email: 'tootctl@example.com', role: '404' } } - - it 'exits with an error message indicating the role name was not found' do - expect { subject } - .to raise_error(Thor::Error, 'Cannot find user role with that name') - end - end - end - - context 'with --reattach option' do - context "when account's user is present" do - let(:options) { { email: 'tootctl_new@example.com', reattach: true } } - let(:user) { Fabricate.build(:user, email: 'tootctl@example.com') } - - before do - Fabricate(:account, username: 'tootctl_username', user: user) - end - - it 'returns an error message indicating the username is already taken' do - expect { subject } - .to output_results("The chosen username is currently in use\nUse --force to reattach it anyway and delete the other user") - end - - context 'with --force option' do - let(:options) { { email: 'tootctl_new@example.com', reattach: true, force: true } } - - it 'reattaches the account to the new user and deletes the previous user' do - expect { subject } - .to output_results('New password') - - user = Account.find_local('tootctl_username')&.user - - expect(user.email).to eq(options[:email]) - end - end - end - - context "when account's user is not present" do - let(:options) { { email: 'tootctl@example.com', reattach: true } } - - before do - Fabricate(:account, username: 'tootctl_username', user: nil) - end - - it_behaves_like 'a new user with given email address and username' - end - end - end - - context 'when required --email option is not provided' do - let(:arguments) { ['tootctl_username'] } - - it 'raises a required argument missing error (Thor::RequiredArgumentMissingError)' do - expect { subject } - .to raise_error(Thor::RequiredArgumentMissingError) - end - end - end - - describe '#modify' do - let(:action) { :modify } - - context 'when the given username is not found' do - let(:arguments) { ['non_existent_username'] } - - it 'exits with an error message indicating the user was not found' do - expect { subject } - .to raise_error(Thor::Error, 'No user with such username') - end - end - - context 'when the given username is found' do - let(:user) { Fabricate(:user) } - let(:arguments) { [user.account.username] } - - context 'when no option is provided' do - it 'returns a successful message and preserves user' do - expect { subject } - .to output_results('OK') - expect(user).to eq(user.reload) - end - end - - context 'with --role option' do - context 'when the given role is not found' do - let(:options) { { role: '404' } } - - it 'exits with an error message indicating the role was not found' do - expect { subject } - .to raise_error(Thor::Error, 'Cannot find user role with that name') - end - end - - context 'when the given role is found' do - let(:default_role) { Fabricate(:user_role) } - let(:options) { { role: default_role.name } } - - it "updates the user's role to the specified role" do - expect { subject } - .to output_results('OK') - - role = user.reload.role - - expect(role.name).to eq(default_role.name) - end - end - end - - context 'with --remove-role option' do - let(:options) { { remove_role: true } } - let(:role) { Fabricate(:user_role) } - let(:user) { Fabricate(:user, role: role) } - - it "removes the user's role successfully" do - expect { subject } - .to output_results('OK') - - role = user.reload.role - - expect(role.name).to be_empty - end - end - - context 'with --email option' do - let(:user) { Fabricate(:user, email: 'old_email@email.com') } - let(:options) { { email: 'new_email@email.com' } } - - it "sets the user's unconfirmed email to the provided email address" do - expect { subject } - .to output_results('OK') - - expect(user.reload.unconfirmed_email).to eq(options[:email]) - end - - it "does not update the user's original email address" do - expect { subject } - .to output_results('OK') - - expect(user.reload.email).to eq('old_email@email.com') - end - - context 'with --confirm option' do - let(:user) { Fabricate(:user, email: 'old_email@email.com', confirmed_at: nil) } - let(:options) { { email: 'new_email@email.com', confirm: true } } - - it "updates the user's email address to the provided email" do - expect { subject } - .to output_results('OK') - - expect(user.reload.email).to eq(options[:email]) - end - - it "sets the user's email address as confirmed" do - expect { subject } - .to output_results('OK') - - expect(user.reload.confirmed?).to be(true) - end - end - end - - context 'with --confirm option' do - let(:user) { Fabricate(:user, confirmed_at: nil) } - let(:options) { { confirm: true } } - - it "confirms the user's email address" do - expect { subject } - .to output_results('OK') - - expect(user.reload.confirmed?).to be(true) - end - end - - context 'with --approve option' do - let(:user) { Fabricate(:user, approved: false) } - let(:options) { { approve: true } } - - before do - Form::AdminSettings.new(registrations_mode: 'approved').save - end - - it 'approves the user' do - expect { subject } - .to output_results('OK') - .and change { user.reload.approved }.from(false).to(true) - end - end - - context 'with --disable option' do - let(:user) { Fabricate(:user, disabled: false) } - let(:options) { { disable: true } } - - it 'disables the user' do - expect { subject } - .to output_results('OK') - .and change { user.reload.disabled }.from(false).to(true) - end - end - - context 'with --enable option' do - let(:user) { Fabricate(:user, disabled: true) } - let(:options) { { enable: true } } - - it 'enables the user' do - expect { subject } - .to output_results('OK') - .and change { user.reload.disabled }.from(true).to(false) - end - end - - context 'with --reset-password option' do - let(:options) { { reset_password: true } } - - it 'returns a new password for the user' do - allow(SecureRandom).to receive(:hex).and_return('new_password') - - expect { subject } - .to output_results('new_password') - end - end - - context 'with --disable-2fa option' do - let(:user) { Fabricate(:user, otp_required_for_login: true) } - let(:options) { { disable_2fa: true } } - - it 'disables the two-factor authentication for the user' do - expect { subject } - .to output_results('OK') - .and change { user.reload.otp_required_for_login }.from(true).to(false) - end - end - - context 'when provided data is invalid' do - let(:user) { Fabricate(:user) } - let(:options) { { email: 'invalid' } } - - it 'exits with an error message' do - expect { subject } - .to raise_error(Thor::Error, %r{Failure/Error: email}) - end - end - end - end - - describe '#delete' do - let(:action) { :delete } - let(:account) { Fabricate(:account) } - let(:delete_account_service) { instance_double(DeleteAccountService) } - - before do - allow(DeleteAccountService).to receive(:new).and_return(delete_account_service) - allow(delete_account_service).to receive(:call) - end - - context 'when both username and --email are provided' do - let(:arguments) { [account.username] } - let(:options) { { email: account.user.email } } - - it 'exits with an error message indicating that only one should be used' do - expect { subject } - .to raise_error(Thor::Error, 'Use username or --email, not both') - end - end - - context 'when neither username nor --email are provided' do - it 'exits with an error message indicating that no username was provided' do - expect { subject } - .to raise_error(Thor::Error, 'No username provided') - end - end - - context 'when username is provided' do - let(:arguments) { [account.username] } - - it 'deletes the specified user successfully' do - expect { subject } - .to output_results('Deleting') - - expect(delete_account_service).to have_received(:call).with(account, reserve_email: false).once - end - - context 'with --dry-run option' do - let(:options) { { dry_run: true } } - - it 'outputs a successful message in dry run mode and does not delete the user' do - expect { subject } - .to output_results('OK (DRY RUN)') - expect(delete_account_service).to_not have_received(:call).with(account, reserve_email: false) - end - end - - context 'when the given username is not found' do - let(:arguments) { ['non_existent_username'] } - - it 'exits with an error message indicating that no user was found' do - expect { subject } - .to raise_error(Thor::Error, 'No user with such username') - end - end - end - - context 'when --email is provided' do - let(:options) { { email: account.user.email } } - - it 'deletes the specified user successfully' do - expect { subject } - .to output_results('Deleting') - - expect(delete_account_service).to have_received(:call).with(account, reserve_email: false).once - end - - context 'with --dry-run option' do - let(:options) { { email: account.user.email, dry_run: true } } - - it 'outputs a successful message in dry run mode and does not delete the user' do - expect { subject } - .to output_results('OK (DRY RUN)') - expect(delete_account_service) - .to_not have_received(:call) - .with(account, reserve_email: false) - end - end - - context 'when the given email address is not found' do - let(:options) { { email: '404@example.com' } } - - it 'exits with an error message indicating that no user was found' do - expect { subject } - .to raise_error(Thor::Error, 'No user with such email') - end - end - end - end - - describe '#approve' do - let(:action) { :approve } - let(:total_users) { 4 } - - before do - Form::AdminSettings.new(registrations_mode: 'approved').save - Fabricate.times(total_users, :user) - end - - context 'with --all option' do - let(:options) { { all: true } } - - it 'approves all pending registrations' do - expect { subject } - .to output_results('OK') - - expect(User.pluck(:approved).all?(true)).to be(true) - end - end - - context 'with --number option' do - context 'when the number is positive' do - let(:options) { { number: 2 } } - - it 'approves the earliest n pending registrations but not the remaining ones' do - expect { subject } - .to output_results('OK') - - expect(n_earliest_pending_registrations.all?(&:approved?)).to be(true) - expect(pending_registrations.all?(&:approved?)).to be(false) - end - - def n_earliest_pending_registrations - User.order(created_at: :asc).first(options[:number]) - end - - def pending_registrations - User.order(created_at: :asc).last(total_users - options[:number]) - end - end - - context 'when the number is negative' do - let(:options) { { number: -1 } } - - it 'exits with an error message indicating that the number must be positive' do - expect { subject } - .to raise_error(Thor::Error, 'Number must be positive') - end - end - - context 'when the given number is greater than the number of users' do - let(:options) { { number: total_users * 2 } } - - it 'approves all users and does not raise any error' do - expect { subject } - .to output_results('OK') - expect(User.pluck(:approved).all?(true)).to be(true) - end - end - end - - context 'with username argument' do - context 'when the given username is found' do - let(:user) { User.last } - let(:arguments) { [user.account.username] } - - it 'approves the specified user successfully' do - expect { subject } - .to output_results('OK') - - expect(user.reload.approved?).to be(true) - end - end - - context 'when the given username is not found' do - let(:arguments) { ['non_existent_username'] } - - it 'exits with an error message indicating that no such account was found' do - expect { subject } - .to raise_error(Thor::Error, 'No such account') - end - end - end - end - - describe '#follow' do - let(:action) { :follow } - - context 'when the given username is not found' do - let(:arguments) { ['non_existent_username'] } - - it 'exits with an error message indicating that no account with the given username was found' do - expect { subject } - .to raise_error(Thor::Error, 'No such account') - end - end - - context 'when the given username is found' do - let!(:target_account) { Fabricate(:account) } - let!(:follower_bob) { Fabricate(:account, username: 'bob') } - let!(:follower_rony) { Fabricate(:account, username: 'rony') } - let!(:follower_charles) { Fabricate(:account, username: 'charles') } - let(:follow_service) { instance_double(FollowService, call: nil) } - let(:arguments) { [target_account.username] } - - before do - allow(FollowService).to receive(:new).and_return(follow_service) - stub_parallelize_with_progress! - end - - it 'displays a successful message and makes all local accounts follow the target account' do - expect { subject } - .to output_results("OK, followed target from #{Account.local.count} accounts") - expect(follow_service).to have_received(:call).with(follower_bob, target_account, any_args).once - expect(follow_service).to have_received(:call).with(follower_rony, target_account, any_args).once - expect(follow_service).to have_received(:call).with(follower_charles, target_account, any_args).once - end - end - end - - describe '#unfollow' do - let(:action) { :unfollow } - - context 'when the given username is not found' do - let(:arguments) { ['non_existent_username'] } - - it 'exits with an error message indicating that no account with the given username was found' do - expect { subject } - .to raise_error(Thor::Error, 'No such account') - end - end - - context 'when the given username is found' do - let!(:target_account) { Fabricate(:account) } - let!(:follower_chris) { Fabricate(:account, username: 'chris', domain: nil) } - let!(:follower_rambo) { Fabricate(:account, username: 'rambo', domain: nil) } - let!(:follower_ana) { Fabricate(:account, username: 'ana', domain: nil) } - let(:unfollow_service) { instance_double(UnfollowService, call: nil) } - let(:arguments) { [target_account.username] } - - before do - accounts = [follower_chris, follower_rambo, follower_ana] - accounts.each { |account| account.follow!(target_account) } - allow(UnfollowService).to receive(:new).and_return(unfollow_service) - stub_parallelize_with_progress! - end - - it 'displays a successful message and makes all local accounts unfollow the target account' do - expect { subject } - .to output_results('OK, unfollowed target from 3 accounts') - expect(unfollow_service).to have_received(:call).with(follower_chris, target_account).once - expect(unfollow_service).to have_received(:call).with(follower_rambo, target_account).once - expect(unfollow_service).to have_received(:call).with(follower_ana, target_account).once - end - end - end - - describe '#backup' do - let(:action) { :backup } - - context 'when the given username is not found' do - let(:arguments) { ['non_existent_username'] } - - it 'exits with an error message indicating that there is no such account' do - expect { subject } - .to raise_error(Thor::Error, 'No user with such username') - end - end - - context 'when the given username is found' do - let(:account) { Fabricate(:account) } - let(:user) { account.user } - let(:arguments) { [account.username] } - - before { allow(BackupWorker).to receive(:perform_async) } - - it 'creates a new backup and backup job for the specified user and outputs success message' do - expect { subject } - .to change { user.backups.count }.by(1) - .and output_results('OK') - expect(BackupWorker).to have_received(:perform_async).with(latest_backup.id).once - end - - def latest_backup - user.backups.last - end - end - end - - describe '#refresh' do - let(:action) { :refresh } - - context 'with --all option' do - let(:options) { { all: true } } - let!(:local_account) { Fabricate(:account, domain: nil) } - let(:remote_com_avatar_url) { 'https://example.host/avatar/com' } - let(:remote_com_header_url) { 'https://example.host/header/com' } - let(:remote_account_example_com) { Fabricate(:account, domain: 'example.com', avatar_remote_url: remote_com_avatar_url, header_remote_url: remote_com_header_url) } - let(:remote_net_avatar_url) { 'https://example.host/avatar/net' } - let(:remote_net_header_url) { 'https://example.host/header/net' } - let(:account_example_net) { Fabricate(:account, domain: 'example.net', avatar_remote_url: remote_net_avatar_url, header_remote_url: remote_net_header_url) } - let(:scope) { Account.remote } - - before do - stub_parallelize_with_progress! - - stub_request(:get, remote_com_avatar_url) - .to_return request_fixture('avatar.txt') - stub_request(:get, remote_com_header_url) - .to_return request_fixture('avatar.txt') - stub_request(:get, remote_net_avatar_url) - .to_return request_fixture('avatar.txt') - stub_request(:get, remote_net_header_url) - .to_return request_fixture('avatar.txt') - - remote_account_example_com - .update_column(:avatar_file_name, nil) - account_example_net - .update_column(:avatar_file_name, nil) - end - - it 'refreshes the avatar and header for all remote accounts' do - expect { subject } - .to output_results('Refreshed 2 accounts') - .and not_change(local_account, :updated_at) - - # One request from factory creation, one more from task - expect(a_request(:get, remote_com_avatar_url)) - .to have_been_made.at_least_times(2) - expect(a_request(:get, remote_com_header_url)) - .to have_been_made.at_least_times(2) - expect(a_request(:get, remote_net_avatar_url)) - .to have_been_made.at_least_times(2) - expect(a_request(:get, remote_net_header_url)) - .to have_been_made.at_least_times(2) - end - - context 'with --dry-run option' do - let(:options) { { all: true, dry_run: true } } - - it 'does not refresh the avatar or header for any account' do - expect { subject } - .to output_results('Refreshed 2 accounts') - - # One request from factory creation, none from task due to dry run - expect(a_request(:get, remote_com_avatar_url)) - .to have_been_made.once - expect(a_request(:get, remote_com_header_url)) - .to have_been_made.once - expect(a_request(:get, remote_net_avatar_url)) - .to have_been_made.once - expect(a_request(:get, remote_net_header_url)) - .to have_been_made.once - end - end - end - - context 'with a list of accts' do - let!(:account_example_com_a) { Fabricate(:account, domain: 'example.com') } - let!(:account_example_com_b) { Fabricate(:account, domain: 'example.com') } - let!(:account_example_net) { Fabricate(:account, domain: 'example.net') } - let(:arguments) { [account_example_com_a.acct, account_example_com_b.acct] } - - before do - # NOTE: `Account.find_remote` is stubbed so that `Account#reset_avatar!` - # can be stubbed on the individual accounts. - allow(Account).to receive(:find_remote).with(account_example_com_a.username, account_example_com_a.domain).and_return(account_example_com_a) - allow(Account).to receive(:find_remote).with(account_example_com_b.username, account_example_com_b.domain).and_return(account_example_com_b) - allow(Account).to receive(:find_remote).with(account_example_net.username, account_example_net.domain).and_return(account_example_net) - end - - it 'resets the avatar for the specified accounts' do - allow(account_example_com_a).to receive(:reset_avatar!) - allow(account_example_com_b).to receive(:reset_avatar!) - - expect { subject } - .to output_results('OK') - - expect(account_example_com_a).to have_received(:reset_avatar!).once - expect(account_example_com_b).to have_received(:reset_avatar!).once - end - - it 'does not reset the avatar for unspecified accounts' do - allow(account_example_net).to receive(:reset_avatar!) - - expect { subject } - .to output_results('OK') - - expect(account_example_net).to_not have_received(:reset_avatar!) - end - - it 'resets the header for the specified accounts' do - allow(account_example_com_a).to receive(:reset_header!) - allow(account_example_com_b).to receive(:reset_header!) - - expect { subject } - .to output_results('OK') - - expect(account_example_com_a).to have_received(:reset_header!).once - expect(account_example_com_b).to have_received(:reset_header!).once - end - - it 'does not reset the header for unspecified accounts' do - allow(account_example_net).to receive(:reset_header!) - - expect { subject } - .to output_results('OK') - - expect(account_example_net).to_not have_received(:reset_header!) - end - - context 'when an UnexpectedResponseError is raised' do - it 'displays a failure message' do - allow(account_example_com_a).to receive(:reset_avatar!).and_raise(Mastodon::UnexpectedResponseError) - - expect { subject } - .to output_results("Account failed: #{account_example_com_a.username}@#{account_example_com_a.domain}") - end - end - - context 'when a specified account is not found' do - it 'exits with an error message' do - allow(Account).to receive(:find_remote).with(account_example_com_b.username, account_example_com_b.domain).and_return(nil) - - expect { subject } - .to raise_error(Thor::Error, 'No such account') - end - end - - context 'with --dry-run option' do - let(:options) { { dry_run: true } } - - it 'does not refresh the avatar for any account' do - allow(account_example_com_a).to receive(:reset_avatar!) - allow(account_example_com_b).to receive(:reset_avatar!) - - expect { subject } - .to output_results('OK (DRY RUN)') - - expect(account_example_com_a).to_not have_received(:reset_avatar!) - expect(account_example_com_b).to_not have_received(:reset_avatar!) - end - - it 'does not refresh the header for any account' do - allow(account_example_com_a).to receive(:reset_header!) - allow(account_example_com_b).to receive(:reset_header!) - - expect { subject } - .to output_results('OK (DRY RUN)') - - expect(account_example_com_a).to_not have_received(:reset_header!) - expect(account_example_com_b).to_not have_received(:reset_header!) - end - end - end - - context 'with --domain option' do - let(:domain) { 'example.com' } - let(:options) { { domain: domain } } - - let(:com_a_avatar_url) { 'https://example.host/avatar/a' } - let(:com_a_header_url) { 'https://example.host/header/a' } - let(:account_example_com_a) { Fabricate(:account, domain: domain, avatar_remote_url: com_a_avatar_url, header_remote_url: com_a_header_url) } - - let(:com_b_avatar_url) { 'https://example.host/avatar/b' } - let(:com_b_header_url) { 'https://example.host/header/b' } - let(:account_example_com_b) { Fabricate(:account, domain: domain, avatar_remote_url: com_b_avatar_url, header_remote_url: com_b_header_url) } - - let(:net_avatar_url) { 'https://example.host/avatar/net' } - let(:net_header_url) { 'https://example.host/header/net' } - let(:account_example_net) { Fabricate(:account, domain: 'example.net', avatar_remote_url: net_avatar_url, header_remote_url: net_header_url) } - - before do - stub_parallelize_with_progress! - - stub_request(:get, com_a_avatar_url) - .to_return request_fixture('avatar.txt') - stub_request(:get, com_a_header_url) - .to_return request_fixture('avatar.txt') - stub_request(:get, com_b_avatar_url) - .to_return request_fixture('avatar.txt') - stub_request(:get, com_b_header_url) - .to_return request_fixture('avatar.txt') - stub_request(:get, net_avatar_url) - .to_return request_fixture('avatar.txt') - stub_request(:get, net_header_url) - .to_return request_fixture('avatar.txt') - - account_example_com_a - .update_column(:avatar_file_name, nil) - account_example_com_b - .update_column(:avatar_file_name, nil) - account_example_net - .update_column(:avatar_file_name, nil) - end - - it 'refreshes the avatar and header for all accounts on specified domain' do - expect { subject } - .to output_results('Refreshed 2 accounts') - - # One request from factory creation, one more from task - expect(a_request(:get, com_a_avatar_url)) - .to have_been_made.at_least_times(2) - expect(a_request(:get, com_a_header_url)) - .to have_been_made.at_least_times(2) - expect(a_request(:get, com_b_avatar_url)) - .to have_been_made.at_least_times(2) - expect(a_request(:get, com_b_header_url)) - .to have_been_made.at_least_times(2) - - # One request from factory creation, none from task - expect(a_request(:get, net_avatar_url)) - .to have_been_made.once - expect(a_request(:get, net_header_url)) - .to have_been_made.once - end - end - - context 'when neither a list of accts nor options are provided' do - it 'exits with an error message' do - expect { subject } - .to raise_error(Thor::Error, 'No account(s) given') - end - end - end - - describe '#rotate' do - let(:action) { :rotate } - - context 'when neither username nor --all option are given' do - it 'exits with an error message' do - expect { subject } - .to raise_error(Thor::Error, 'No account(s) given') - end - end - - context 'when a username is given' do - let(:account) { Fabricate(:account) } - let(:arguments) { [account.username] } - - it 'correctly rotates keys for the specified account' do - old_private_key = account.private_key - old_public_key = account.public_key - - expect { subject } - .to output_results('OK') - account.reload - - expect(account.private_key).to_not eq(old_private_key) - expect(account.public_key).to_not eq(old_public_key) - end - - it 'broadcasts the new keys for the specified account' do - allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in) - - expect { subject } - .to output_results('OK') - - expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id, anything).once - end - end - - context 'when the given username is not found' do - let(:arguments) { ['non_existent_username'] } - - it 'exits with an error message when the specified username is not found' do - expect { subject } - .to raise_error(Thor::Error, 'No such account') - end - end - - context 'when --all option is provided' do - let!(:accounts) { Fabricate.times(2, :account) } - let(:options) { { all: true } } - - it 'correctly rotates keys for all local accounts' do - old_private_keys = accounts.map(&:private_key) - old_public_keys = accounts.map(&:public_key) - - expect { subject } - .to output_results('rotated') - accounts.each(&:reload) - - expect(accounts.map(&:private_key)).to_not eq(old_private_keys) - expect(accounts.map(&:public_key)).to_not eq(old_public_keys) - end - - it 'broadcasts the new keys for each account' do - allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in) - - expect { subject } - .to output_results('rotated') - - accounts.each do |account| - expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id, anything).once - end - end - end - end - - describe '#merge' do - let(:action) { :merge } - - shared_examples 'an account not found' do |acct| - it 'exits with an error message indicating that there is no such account' do - expect { subject } - .to raise_error(Thor::Error, "No such account (#{acct})") - end - end - - context 'when "from_account" is not found' do - let(:to_account) { Fabricate(:account, domain: 'example.com') } - let(:arguments) { ['non_existent_username@domain.com', "#{to_account.username}@#{to_account.domain}"] } - - it_behaves_like 'an account not found', 'non_existent_username@domain.com' - end - - context 'when "from_account" is a local account' do - let(:from_account) { Fabricate(:account, domain: nil, username: 'bob') } - let(:to_account) { Fabricate(:account, domain: 'example.com') } - let(:arguments) { [from_account.username, "#{to_account.username}@#{to_account.domain}"] } - - it_behaves_like 'an account not found', 'bob' - end - - context 'when "to_account" is not found' do - let(:from_account) { Fabricate(:account, domain: 'example.com') } - let(:arguments) { ["#{from_account.username}@#{from_account.domain}", 'non_existent_username'] } - - it_behaves_like 'an account not found', 'non_existent_username' - end - - context 'when "to_account" is local' do - let(:from_account) { Fabricate(:account, domain: 'example.com') } - let(:to_account) { Fabricate(:account, domain: nil, username: 'bob') } - let(:arguments) do - ["#{from_account.username}@#{from_account.domain}", "#{to_account.username}@#{to_account.domain}"] - end - - it_behaves_like 'an account not found', 'bob@' - end - - context 'when "from_account" and "to_account" public keys do not match' do - let(:from_account) { instance_double(Account, username: 'bob', domain: 'example1.com', local?: false, public_key: 'from_account') } - let(:to_account) { instance_double(Account, username: 'bob', domain: 'example2.com', local?: false, public_key: 'to_account') } - let(:arguments) do - ["#{from_account.username}@#{from_account.domain}", "#{to_account.username}@#{to_account.domain}"] - end - - before do - allow(Account).to receive(:find_remote).with(from_account.username, from_account.domain).and_return(from_account) - allow(Account).to receive(:find_remote).with(to_account.username, to_account.domain).and_return(to_account) - end - - it 'exits with an error message indicating that the accounts do not have the same pub key' do - expect { subject } - .to raise_error(Thor::Error, "Accounts don't have the same public key, might not be duplicates!\nOverride with --force\n") - end - - context 'with --force option' do - let(:options) { { force: true } } - - before do - allow(to_account).to receive(:merge_with!) - allow(from_account).to receive(:destroy) - end - - it 'merges `from_account` into `to_account` and deletes `from_account`' do - expect { subject } - .to output_results('OK') - - expect(to_account).to have_received(:merge_with!).with(from_account).once - expect(from_account).to have_received(:destroy).once - end - end - end - - context 'when "from_account" and "to_account" public keys match' do - let(:from_account) { instance_double(Account, username: 'bob', domain: 'example1.com', local?: false, public_key: 'pub_key') } - let(:to_account) { instance_double(Account, username: 'bob', domain: 'example2.com', local?: false, public_key: 'pub_key') } - let(:arguments) do - ["#{from_account.username}@#{from_account.domain}", "#{to_account.username}@#{to_account.domain}"] - end - - before do - allow(Account).to receive(:find_remote).with(from_account.username, from_account.domain).and_return(from_account) - allow(Account).to receive(:find_remote).with(to_account.username, to_account.domain).and_return(to_account) - allow(to_account).to receive(:merge_with!) - allow(from_account).to receive(:destroy) - end - - it 'merges "from_account" into "to_account" and deletes from_account' do - expect { subject } - .to output_results('OK') - - expect(to_account).to have_received(:merge_with!).with(from_account).once - expect(from_account).to have_received(:destroy) - end - end - end - - describe '#cull' do - let(:action) { :cull } - let(:delete_account_service) { instance_double(DeleteAccountService, call: nil) } - let!(:tom) { Fabricate(:account, updated_at: 30.days.ago, username: 'tom', uri: 'https://example.com/users/tom', domain: 'example.com', protocol: :activitypub) } - let!(:bob) { Fabricate(:account, updated_at: 30.days.ago, last_webfingered_at: nil, username: 'bob', uri: 'https://example.org/users/bob', domain: 'example.org', protocol: :activitypub) } - let!(:gon) { Fabricate(:account, updated_at: 15.days.ago, last_webfingered_at: 15.days.ago, username: 'gon', uri: 'https://example.net/users/gon', domain: 'example.net', protocol: :activitypub) } - let!(:ana) { Fabricate(:account, username: 'ana', uri: 'https://example.com/users/ana', domain: 'example.com', protocol: :activitypub) } - let!(:tales) { Fabricate(:account, updated_at: 10.days.ago, last_webfingered_at: nil, username: 'tales', uri: 'https://example.net/users/tales', domain: 'example.net', protocol: :activitypub) } - - before do - allow(DeleteAccountService).to receive(:new).and_return(delete_account_service) - end - - context 'when no domain is specified' do - before do - stub_parallelize_with_progress! - stub_request(:head, 'https://example.org/users/bob').to_return(status: 404) - stub_request(:head, 'https://example.net/users/gon').to_return(status: 410) - stub_request(:head, 'https://example.net/users/tales').to_return(status: 200) - end - - def expect_delete_inactive_remote_accounts - expect(delete_account_service).to have_received(:call).with(bob, reserve_username: false).once - expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once - end - - def expect_not_delete_active_accounts - expect(delete_account_service).to_not have_received(:call).with(tom, reserve_username: false) - expect(delete_account_service).to_not have_received(:call).with(ana, reserve_username: false) - expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false) - end - - it 'touches inactive remote accounts that have not been deleted and summarizes activity' do - expect { subject } - .to change { tales.reload.updated_at } - .and output_results('Visited 5 accounts, removed 2') - expect_delete_inactive_remote_accounts - expect_not_delete_active_accounts - end - end - - context 'when a domain is specified' do - let(:arguments) { ['example.net'] } - - before do - stub_parallelize_with_progress! - stub_request(:head, 'https://example.net/users/gon').to_return(status: 410) - stub_request(:head, 'https://example.net/users/tales').to_return(status: 404) - end - - def expect_delete_inactive_remote_accounts - expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once - expect(delete_account_service).to have_received(:call).with(tales, reserve_username: false).once - end - - it 'displays the summary correctly and deletes inactive remote accounts' do - expect { subject } - .to output_results('Visited 2 accounts, removed 2') - expect_delete_inactive_remote_accounts - end - end - - context 'when a domain is unavailable' do - shared_examples 'an unavailable domain' do - before do - stub_parallelize_with_progress! - stub_request(:head, 'https://example.org/users/bob').to_return(status: 200) - stub_request(:head, 'https://example.net/users/gon').to_return(status: 200) - end - - def expect_skip_accounts_from_unavailable_domain - expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false) - end - - it 'displays the summary correctly and skip accounts from unavailable domains' do - expect { subject } - .to output_results("Visited 5 accounts, removed 0\nThe following domains were not available during the check:\n example.net") - expect_skip_accounts_from_unavailable_domain - end - end - - context 'when a connection timeout occurs' do - before do - stub_request(:head, 'https://example.net/users/tales').to_timeout - end - - it_behaves_like 'an unavailable domain' - end - - context 'when a connection error occurs' do - before do - stub_request(:head, 'https://example.net/users/tales').to_raise(HTTP::ConnectionError) - end - - it_behaves_like 'an unavailable domain' - end - - context 'when an ssl error occurs' do - before do - stub_request(:head, 'https://example.net/users/tales').to_raise(OpenSSL::SSL::SSLError) - end - - it_behaves_like 'an unavailable domain' - end - - context 'when a private network address error occurs' do - before do - stub_request(:head, 'https://example.net/users/tales').to_raise(Mastodon::PrivateNetworkAddressError) - end - - it_behaves_like 'an unavailable domain' - end - end - end - - describe '#reset_relationships' do - let(:action) { :reset_relationships } - let(:target_account) { Fabricate(:account) } - let(:arguments) { [target_account.username] } - - context 'when no option is given' do - it 'exits with an error message indicating that at least one option is required' do - expect { subject } - .to raise_error(Thor::Error, 'Please specify either --follows or --followers, or both') - end - end - - context 'when the given username is not found' do - let(:arguments) { ['non_existent_username'] } - let(:options) { { follows: true } } - - it 'exits with an error message indicating that there is no such account' do - expect { subject } - .to raise_error(Thor::Error, 'No such account') - end - end - - context 'when the given username is found' do - let(:total_relationships) { 3 } - let!(:accounts) { Fabricate.times(total_relationships, :account) } - - context 'with --follows option' do - let(:options) { { follows: true } } - - before do - accounts.each { |account| target_account.follow!(account) } - allow(BootstrapTimelineWorker).to receive(:perform_async) - end - - it 'resets following relationships and displays a successful message and rebuilds timeline' do - expect { subject } - .to output_results("Processed #{total_relationships} relationships") - expect(target_account.reload.following).to be_empty - expect(BootstrapTimelineWorker).to have_received(:perform_async).with(target_account.id).once - end - end - - context 'with --followers option' do - let(:options) { { followers: true } } - - before do - accounts.each { |account| account.follow!(target_account) } - end - - it 'resets followers relationships and displays a successful message' do - expect { subject } - .to output_results("Processed #{total_relationships} relationships") - expect(target_account.reload.followers).to be_empty - end - end - - context 'with --follows and --followers options' do - let(:options) { { followers: true, follows: true } } - - before do - accounts.first(2).each { |account| account.follow!(target_account) } - accounts.last(1).each { |account| target_account.follow!(account) } - allow(BootstrapTimelineWorker).to receive(:perform_async) - end - - it 'resets followers and following and displays a successful message and rebuilds timeline' do - expect { subject } - .to output_results("Processed #{total_relationships} relationships") - expect(target_account.reload.followers).to be_empty - expect(target_account.reload.following).to be_empty - expect(BootstrapTimelineWorker).to have_received(:perform_async).with(target_account.id).once - end - end - end - end - - describe '#prune' do - let(:action) { :prune } - let!(:local_account) { Fabricate(:account) } - let!(:bot_account) { Fabricate(:account, bot: true, domain: 'example.com') } - let!(:group_account) { Fabricate(:account, actor_type: 'Group', domain: 'example.com') } - let!(:mentioned_account) { Fabricate(:account, domain: 'example.com') } - let!(:prunable_accounts) do - Fabricate.times(2, :account, domain: 'example.com', bot: false, suspended_at: nil, silenced_at: nil) - end - - before do - Fabricate(:mention, account: mentioned_account, status: Fabricate(:status, account: Fabricate(:account))) - stub_parallelize_with_progress! - end - - def expect_prune_remote_accounts_without_interaction - prunable_account_ids = prunable_accounts.pluck(:id) - - expect(Account.where(id: prunable_account_ids).count).to eq(0) - end - - it 'displays a successful message and handles accounts correctly' do - expect { subject } - .to output_results("OK, pruned #{prunable_accounts.size} accounts") - expect_prune_remote_accounts_without_interaction - expect_not_prune_local_accounts - expect_not_prune_bot_accounts - expect_not_prune_group_accounts - expect_not_prune_mentioned_accounts - end - - def expect_not_prune_local_accounts - expect(Account.exists?(id: local_account.id)).to be(true) - end - - def expect_not_prune_bot_accounts - expect(Account.exists?(id: bot_account.id)).to be(true) - end - - def expect_not_prune_group_accounts - expect(Account.exists?(id: group_account.id)).to be(true) - end - - def expect_not_prune_mentioned_accounts - expect(Account.exists?(id: mentioned_account.id)).to be true - end - - context 'with --dry-run option' do - let(:options) { { dry_run: true } } - - def expect_no_account_prunes - prunable_account_ids = prunable_accounts.pluck(:id) - - expect(Account.where(id: prunable_account_ids).count).to eq(prunable_accounts.size) - end - - it 'displays a successful message with (DRY RUN) and doesnt prune anything' do - expect { subject } - .to output_results("OK, pruned #{prunable_accounts.size} accounts (DRY RUN)") - expect_no_account_prunes - end - end - end - - describe '#migrate' do - let(:action) { :migrate } - let!(:source_account) { Fabricate(:account) } - let!(:target_account) { Fabricate(:account, domain: 'example.com') } - let(:arguments) { [source_account.username] } - let(:resolve_account_service) { instance_double(ResolveAccountService, call: nil) } - let(:move_service) { instance_double(MoveService, call: nil) } - - before do - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service) - allow(MoveService).to receive(:new).and_return(move_service) - end - - shared_examples 'a successful migration' do - it 'displays a success message and calls the MoveService for the last migration' do - expect { subject } - .to output_results("OK, migrated #{source_account.acct} to #{target_account.acct}") - - expect(move_service) - .to have_received(:call).with(last_migration).once - end - - def last_migration - source_account.migrations.last - end - end - - context 'when both --replay and --target options are given' do - let(:options) { { replay: true, target: "#{target_account.username}@example.com" } } - - it 'exits with an error message indicating that using both options is not possible' do - expect { subject } - .to raise_error(Thor::Error, 'Use --replay or --target, not both') - end - end - - context 'when no option is given' do - it 'exits with an error message indicating that at least one option must be used' do - expect { subject } - .to raise_error(Thor::Error, 'Use either --replay or --target') - end - end - - context 'when the given username is not found' do - let(:arguments) { ['non_existent_username'] } - let(:options) { { replay: true } } - - it 'exits with an error message indicating that there is no such account' do - expect { subject } - .to raise_error(Thor::Error, "No such account: #{arguments.first}") - end - end - - context 'with --replay option' do - let(:options) { { replay: true } } - - context 'when the specified account has no previous migrations' do - it 'exits with an error message indicating that the given account has no previous migrations' do - expect { subject } - .to raise_error(Thor::Error, 'The specified account has not performed any migration') - end - end - - context 'when the specified account has a previous migration' do - before do - allow(resolve_account_service).to receive(:call).with(source_account.acct, any_args).and_return(source_account) - allow(resolve_account_service).to receive(:call).with(target_account.acct, any_args).and_return(target_account) - target_account.aliases.create!(acct: source_account.acct) - source_account.migrations.create!(acct: target_account.acct) - source_account.update!(moved_to_account: target_account) - end - - it_behaves_like 'a successful migration' - - context 'when the specified account is redirecting to a different target account' do - before do - source_account.update!(moved_to_account: nil) - end - - it 'exits with an error message' do - expect { subject } - .to raise_error(Thor::Error, 'The specified account is not redirecting to its last migration target. Use --force if you want to replay the migration anyway') - end - end - - context 'with --force option' do - let(:options) { { replay: true, force: true } } - - it_behaves_like 'a successful migration' - end - end - end - - context 'with --target option' do - let(:options) { { target: target_account.acct } } - - before do - allow(resolve_account_service).to receive(:call).with(source_account.acct, any_args).and_return(source_account) - allow(resolve_account_service).to receive(:call).with(target_account.acct, any_args).and_return(target_account) - end - - context 'when the specified target account is not found' do - before do - allow(resolve_account_service).to receive(:call).with(target_account.acct).and_return(nil) - end - - it 'exits with an error message indicating that there is no such account' do - expect { subject } - .to raise_error(Thor::Error, "The specified target account could not be found: #{options[:target]}") - end - end - - context 'when the specified target account exists' do - before do - target_account.aliases.create!(acct: source_account.acct) - end - - it 'creates a migration for the specified account with the target account' do - expect { subject } - .to output_results('migrated') - - last_migration = source_account.migrations.last - - expect(last_migration.acct).to eq(target_account.acct) - end - - it_behaves_like 'a successful migration' - end - - context 'when the migration record is invalid' do - it 'exits with an error indicating that the validation failed' do - expect { subject } - .to raise_error(Thor::Error, /Error: Validation failed/) - end - end - - context 'when the specified account is redirecting to a different target account' do - before do - source_account.update(moved_to_account: Fabricate(:account)) - end - - it 'exits with an error message' do - expect { subject } - .to raise_error(Thor::Error, 'The specified account is redirecting to a different target account. Use --force if you want to change the migration target') - end - end - - context 'with --target and --force options' do - let(:options) { { target: target_account.acct, force: true } } - - before do - source_account.update(moved_to_account: Fabricate(:account)) - target_account.aliases.create!(acct: source_account.acct) - end - - it_behaves_like 'a successful migration' - end - end - end -end diff --git a/spec/lib/mastodon/cli/cache_spec.rb b/spec/lib/mastodon/cli/cache_spec.rb deleted file mode 100644 index 247a14f9e2..0000000000 --- a/spec/lib/mastodon/cli/cache_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/cache' - -describe Mastodon::CLI::Cache do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#clear' do - let(:action) { :clear } - - before { allow(Rails.cache).to receive(:clear) } - - it 'clears the Rails cache' do - expect { subject } - .to output_results('OK') - expect(Rails.cache).to have_received(:clear) - end - end - - describe '#recount' do - let(:action) { :recount } - - context 'with the `accounts` argument' do - let(:arguments) { ['accounts'] } - let(:account_stat) { Fabricate(:account_stat) } - - before do - account_stat.update(statuses_count: 123) - end - - it 're-calculates account records in the cache' do - expect { subject } - .to output_results('OK') - - expect(account_stat.reload.statuses_count).to be_zero - end - end - - context 'with the `statuses` argument' do - let(:arguments) { ['statuses'] } - let(:status_stat) { Fabricate(:status_stat) } - - before do - status_stat.update(replies_count: 123) - end - - it 're-calculates account records in the cache' do - expect { subject } - .to output_results('OK') - - expect(status_stat.reload.replies_count).to be_zero - end - end - - context 'with an unknown type' do - let(:arguments) { ['other-type'] } - - it 'Exits with an error message' do - expect { subject } - .to raise_error(Thor::Error, /Unknown/) - end - end - end -end diff --git a/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb b/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb deleted file mode 100644 index 1745ea01bf..0000000000 --- a/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/canonical_email_blocks' - -describe Mastodon::CLI::CanonicalEmailBlocks do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#find' do - let(:action) { :find } - let(:arguments) { ['user@example.com'] } - - context 'when a block is present' do - before { Fabricate(:canonical_email_block, email: 'user@example.com') } - - it 'announces the presence of the block' do - expect { subject } - .to output_results('user@example.com is blocked') - end - end - - context 'when a block is not present' do - it 'announces the absence of the block' do - expect { subject } - .to output_results('user@example.com is not blocked') - end - end - end - - describe '#remove' do - let(:action) { :remove } - let(:arguments) { ['user@example.com'] } - - context 'when a block is present' do - before { Fabricate(:canonical_email_block, email: 'user@example.com') } - - it 'removes the block' do - expect { subject } - .to output_results('Unblocked user@example.com') - - expect(CanonicalEmailBlock.matching_email('user@example.com')).to be_empty - end - end - - context 'when a block is not present' do - it 'announces the absence of the block' do - expect { subject } - .to output_results('user@example.com is not blocked') - end - end - end -end diff --git a/spec/lib/mastodon/cli/domains_spec.rb b/spec/lib/mastodon/cli/domains_spec.rb deleted file mode 100644 index 448e6fe42b..0000000000 --- a/spec/lib/mastodon/cli/domains_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/domains' - -describe Mastodon::CLI::Domains do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#purge' do - let(:action) { :purge } - - context 'with invalid limited federation mode argument' do - let(:arguments) { ['example.host'] } - let(:options) { { limited_federation_mode: true } } - - it 'warns about usage and exits' do - expect { subject } - .to raise_error(Thor::Error, /DOMAIN parameter not supported/) - end - end - - context 'without a domains argument' do - it 'warns about usage and exits' do - expect { subject } - .to raise_error(Thor::Error, 'No domain(s) given') - end - end - - context 'with accounts from the domain' do - let(:domain) { 'host.example' } - let!(:account) { Fabricate(:account, domain: domain) } - let(:arguments) { [domain] } - - it 'removes the account' do - expect { subject } - .to output_results('Removed 1 accounts') - - expect { account.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end - - describe '#crawl' do - let(:action) { :crawl } - - context 'with accounts from the domain' do - let(:domain) { 'host.example' } - - before do - Fabricate(:account, domain: domain) - stub_request(:get, 'https://host.example/api/v1/instance').to_return(status: 200, body: {}.to_json) - stub_request(:get, 'https://host.example/api/v1/instance/peers').to_return(status: 200, body: {}.to_json) - stub_request(:get, 'https://host.example/api/v1/instance/activity').to_return(status: 200, body: {}.to_json) - stub_const('Mastodon::CLI::Domains::CRAWL_SLEEP_TIME', 0) - end - - context 'with --format of summary' do - let(:options) { { format: 'summary' } } - - it 'crawls the domains and summarizes results' do - expect { subject } - .to output_results('Visited 1 domains, 0 failed') - end - end - - context 'with --format of domains' do - let(:options) { { format: 'domains' } } - - it 'crawls the domains and summarizes results' do - expect { subject } - .to output_results(domain) - end - end - - context 'with --format of json' do - let(:options) { { format: 'json' } } - - it 'crawls the domains and summarizes results' do - expect { subject } - .to output_results(json_summary) - end - - def json_summary - Oj.dump('host.example': { activity: {} }) - end - end - end - end -end diff --git a/spec/lib/mastodon/cli/email_domain_blocks_spec.rb b/spec/lib/mastodon/cli/email_domain_blocks_spec.rb deleted file mode 100644 index 55e3da0bb8..0000000000 --- a/spec/lib/mastodon/cli/email_domain_blocks_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/email_domain_blocks' - -describe Mastodon::CLI::EmailDomainBlocks do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#list' do - let(:action) { :list } - - context 'with email domain block records' do - let!(:parent_block) { Fabricate(:email_domain_block) } - let!(:child_block) { Fabricate(:email_domain_block, parent: parent_block) } - - it 'lists the blocks' do - expect { subject } - .to output_results( - parent_block.domain, - child_block.domain - ) - end - end - end - - describe '#add' do - let(:action) { :add } - - context 'without any options' do - it 'warns about usage and exits' do - expect { subject } - .to raise_error(Thor::Error, 'No domain(s) given') - end - end - - context 'when blocks exist' do - let(:options) { {} } - let(:domain) { 'host.example' } - let(:arguments) { [domain] } - - before { Fabricate(:email_domain_block, domain: domain) } - - it 'does not add a new block' do - expect { subject } - .to output_results('is already blocked') - .and(not_change(EmailDomainBlock, :count)) - end - end - - context 'when no blocks exist' do - let(:domain) { 'host.example' } - let(:arguments) { [domain] } - - it 'adds a new block' do - expect { subject } - .to output_results('Added 1') - .and(change(EmailDomainBlock, :count).by(1)) - end - end - end - - describe '#remove' do - let(:action) { :remove } - - context 'without any options' do - it 'warns about usage and exits' do - expect { subject } - .to raise_error(Thor::Error, 'No domain(s) given') - end - end - - context 'when blocks exist' do - let(:domain) { 'host.example' } - let(:arguments) { [domain] } - - before { Fabricate(:email_domain_block, domain: domain) } - - it 'removes the block' do - expect { subject } - .to output_results('Removed 1') - .and(change(EmailDomainBlock, :count).by(-1)) - end - end - - context 'when no blocks exist' do - let(:domain) { 'host.example' } - let(:arguments) { [domain] } - - it 'does not remove a block' do - expect { subject } - .to output_results('is not yet blocked') - .and(not_change(EmailDomainBlock, :count)) - end - end - end -end diff --git a/spec/lib/mastodon/cli/emoji_spec.rb b/spec/lib/mastodon/cli/emoji_spec.rb deleted file mode 100644 index d05e972e77..0000000000 --- a/spec/lib/mastodon/cli/emoji_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/emoji' - -describe Mastodon::CLI::Emoji do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#purge' do - let(:action) { :purge } - - context 'with existing custom emoji' do - before { Fabricate(:custom_emoji) } - - it 'reports a successful purge' do - expect { subject } - .to output_results('OK') - end - end - end - - describe '#import' do - context 'with existing custom emoji' do - let(:import_path) { Rails.root.join('spec', 'fixtures', 'files', 'elite-assets.tar.gz') } - let(:action) { :import } - let(:arguments) { [import_path] } - - it 'reports about imported emoji' do - expect { subject } - .to output_results('Imported 1') - .and change(CustomEmoji, :count).by(1) - end - end - end - - describe '#export' do - context 'with existing custom emoji' do - before do - FileUtils.rm_rf(export_path.dirname) - FileUtils.mkdir_p(export_path.dirname) - - Fabricate(:custom_emoji) - end - - after { FileUtils.rm_rf(export_path.dirname) } - - let(:export_path) { Rails.root.join('tmp', 'cli-tests', 'export.tar.gz') } - let(:arguments) { [export_path.dirname.to_s] } - let(:action) { :export } - - it 'reports about exported emoji' do - expect { subject } - .to output_results('Exported 1') - .and change { File.exist?(export_path) }.from(false).to(true) - end - end - end -end diff --git a/spec/lib/mastodon/cli/feeds_spec.rb b/spec/lib/mastodon/cli/feeds_spec.rb deleted file mode 100644 index 420cb3d587..0000000000 --- a/spec/lib/mastodon/cli/feeds_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/feeds' - -describe Mastodon::CLI::Feeds do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#build' do - let(:action) { :build } - - before { Fabricate(:account) } - - context 'with --all option' do - let(:options) { { all: true } } - - it 'regenerates feeds for all accounts' do - expect { subject } - .to output_results('Regenerated feeds') - end - end - - context 'with a username' do - before { Fabricate(:account, username: 'alice') } - - let(:arguments) { ['alice'] } - - it 'regenerates feeds for the account' do - expect { subject } - .to output_results('OK') - end - end - - context 'with invalid username' do - let(:arguments) { ['invalid-username'] } - - it 'displays an error and exits' do - expect { subject } - .to raise_error(Thor::Error, 'No such account') - end - end - end - - describe '#clear' do - let(:action) { :clear } - - before do - allow(redis).to receive(:del).with(key_namespace) - end - - it 'clears the redis `feed:*` namespace' do - expect { subject } - .to output_results('OK') - - expect(redis).to have_received(:del).with(key_namespace).once - end - - def key_namespace - redis.keys('feed:*') - end - end -end diff --git a/spec/lib/mastodon/cli/ip_blocks_spec.rb b/spec/lib/mastodon/cli/ip_blocks_spec.rb deleted file mode 100644 index d44b1b9fe4..0000000000 --- a/spec/lib/mastodon/cli/ip_blocks_spec.rb +++ /dev/null @@ -1,284 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/ip_blocks' - -describe Mastodon::CLI::IpBlocks do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#add' do - let(:action) { :add } - let(:ip_list) do - [ - '192.0.2.1', - '172.16.0.1', - '192.0.2.0/24', - '172.16.0.0/16', - '10.0.0.0/8', - '2001:0db8:85a3:0000:0000:8a2e:0370:7334', - 'fe80::1', - '::1', - '2001:0db8::/32', - 'fe80::/10', - '::/128', - ] - end - let(:options) { { severity: 'no_access' } } - let(:arguments) { ip_list } - - shared_examples 'ip address blocking' do - def blocked_ip_addresses - IpBlock.where(ip: ip_list).pluck(:ip) - end - - def expected_ip_addresses - ip_list.map { |ip| IPAddr.new(ip) } - end - - def blocked_ips_severity - IpBlock.where(ip: ip_list).pluck(:severity).all?(options[:severity]) - end - - it 'blocks and sets severity for ip address and displays summary' do - expect { subject } - .to output_results("Added #{ip_list.size}, skipped 0, failed 0") - expect(blocked_ip_addresses) - .to match_array(expected_ip_addresses) - expect(blocked_ips_severity) - .to be(true) - end - end - - context 'with valid IP addresses' do - include_examples 'ip address blocking' - end - - context 'when a specified IP address is already blocked' do - let!(:blocked_ip) { IpBlock.create(ip: ip_list.last, severity: options[:severity]) } - let(:arguments) { ip_list } - - before { allow(IpBlock).to receive(:new).and_call_original } - - it 'skips already block ip and displays the correct summary' do - expect { subject } - .to output_results("#{ip_list.last} is already blocked\nAdded #{ip_list.size - 1}, skipped 1, failed 0") - - expect(IpBlock).to_not have_received(:new).with(ip: ip_list.last) - end - - context 'with --force option' do - let!(:blocked_ip) { IpBlock.create(ip: ip_list.last, severity: 'no_access') } - let(:options) { { severity: 'sign_up_requires_approval', force: true } } - - it 'overwrites the existing IP block record' do - expect { subject } - .to output_results('Added 11') - .and change { blocked_ip.reload.severity } - .from('no_access') - .to('sign_up_requires_approval') - end - - include_examples 'ip address blocking' - end - end - - context 'when a specified IP address is invalid' do - let(:ip_list) { ['320.15.175.0', '9.5.105.255', '0.0.0.0'] } - let(:arguments) { ip_list } - - it 'displays the correct summary' do - expect { subject } - .to output_results("#{ip_list.first} is invalid\nAdded #{ip_list.size - 1}, skipped 0, failed 1") - end - end - - context 'with --comment option' do - let(:options) { { severity: 'no_access', comment: 'Spam' } } - - include_examples 'ip address blocking' - end - - context 'with --duration option' do - let(:options) { { severity: 'no_access', duration: 10.days } } - - include_examples 'ip address blocking' - end - - context 'with "sign_up_requires_approval" severity' do - let(:options) { { severity: 'sign_up_requires_approval' } } - - include_examples 'ip address blocking' - end - - context 'with "sign_up_block" severity' do - let(:options) { { severity: 'sign_up_block' } } - - include_examples 'ip address blocking' - end - - context 'when a specified IP address fails to be blocked' do - let(:ip_address) { '127.0.0.1' } - let(:ip_block) { instance_double(IpBlock, ip: ip_address, save: false) } - let(:arguments) { [ip_address] } - - before do - allow(IpBlock).to receive(:new).and_return(ip_block) - allow(ip_block).to receive(:severity=) - allow(ip_block).to receive(:expires_in=) - end - - it 'displays an error message' do - expect { subject } - .to output_results("#{ip_address} could not be saved") - end - end - - context 'when no IP address is provided' do - let(:arguments) { [] } - - it 'exits with an error message' do - expect { subject } - .to raise_error(Thor::Error, 'No IP(s) given') - end - end - end - - describe '#remove' do - let(:action) { :remove } - - context 'when removing exact matches' do - let(:ip_list) do - [ - '192.0.2.1', - '172.16.0.1', - '192.0.2.0/24', - '172.16.0.0/16', - '10.0.0.0/8', - '2001:0db8:85a3:0000:0000:8a2e:0370:7334', - 'fe80::1', - '::1', - '2001:0db8::/32', - 'fe80::/10', - '::/128', - ] - end - let(:arguments) { ip_list } - - before do - ip_list.each { |ip| IpBlock.create(ip: ip, severity: :no_access) } - end - - it 'removes exact ip blocks and displays success message with a summary' do - expect { subject } - .to output_results("Removed #{ip_list.size}, skipped 0") - expect(IpBlock.where(ip: ip_list)).to_not exist - end - end - - context 'with --force option' do - let!(:first_ip_range_block) { IpBlock.create(ip: '192.168.0.0/24', severity: :no_access) } - let!(:second_ip_range_block) { IpBlock.create(ip: '10.0.0.0/16', severity: :no_access) } - let!(:third_ip_range_block) { IpBlock.create(ip: '172.16.0.0/20', severity: :no_access) } - let(:arguments) { ['192.168.0.5', '10.0.1.50'] } - let(:options) { { force: true } } - - it 'removes blocks for IP ranges that cover given IP(s) and keeps other ranges' do - expect { subject } - .to output_results('Removed 2') - - expect(covered_ranges).to_not exist - expect(other_ranges).to exist - end - - def covered_ranges - IpBlock.where(id: [first_ip_range_block.id, second_ip_range_block.id]) - end - - def other_ranges - IpBlock.where(id: third_ip_range_block.id) - end - end - - context 'when a specified IP address is not blocked' do - let(:unblocked_ip) { '192.0.2.1' } - let(:arguments) { [unblocked_ip] } - - it 'skips the IP address and displays summary' do - expect { subject } - .to output_results( - "#{unblocked_ip} is not yet blocked", - 'Removed 0, skipped 1' - ) - end - end - - context 'when a specified IP address is invalid' do - let(:invalid_ip) { '320.15.175.0' } - let(:arguments) { [invalid_ip] } - - it 'skips the invalid IP address and displays summary' do - expect { subject } - .to output_results( - "#{invalid_ip} is invalid", - 'Removed 0, skipped 1' - ) - end - end - - context 'when no IP address is provided' do - it 'exits with an error message' do - expect { subject } - .to raise_error(Thor::Error, 'No IP(s) given') - end - end - end - - describe '#export' do - let(:action) { :export } - - let(:first_ip_range_block) { IpBlock.create(ip: '192.168.0.0/24', severity: :no_access) } - let(:second_ip_range_block) { IpBlock.create(ip: '10.0.0.0/16', severity: :no_access) } - let(:third_ip_range_block) { IpBlock.create(ip: '127.0.0.1', severity: :sign_up_block) } - - context 'when --format option is set to "plain"' do - let(:options) { { format: 'plain' } } - - it 'exports blocked IPs with "no_access" severity in plain format' do - expect { subject } - .to output_results("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}") - end - - it 'does not export blocked IPs with different severities' do - expect { subject } - .to_not output_results("#{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}") - end - end - - context 'when --format option is set to "nginx"' do - let(:options) { { format: 'nginx' } } - - it 'exports blocked IPs with "no_access" severity in plain format' do - expect { subject } - .to output_results("deny #{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};\ndeny #{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix};") - end - - it 'does not export blocked IPs with different severities' do - expect { subject } - .to_not output_results("deny #{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};") - end - end - - context 'when --format option is not provided' do - it 'exports blocked IPs in plain format by default' do - expect { subject } - .to output_results("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}") - end - end - end -end diff --git a/spec/lib/mastodon/cli/main_spec.rb b/spec/lib/mastodon/cli/main_spec.rb deleted file mode 100644 index 99d770a81d..0000000000 --- a/spec/lib/mastodon/cli/main_spec.rb +++ /dev/null @@ -1,176 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/main' - -describe Mastodon::CLI::Main do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#version' do - let(:action) { :version } - - it 'returns the Mastodon version' do - expect { subject } - .to output_results(Mastodon::Version.to_s) - end - end - - describe '#self_destruct' do - let(:action) { :self_destruct } - - context 'with self destruct mode enabled' do - before do - allow(SelfDestructHelper).to receive(:self_destruct?).and_return(true) - end - - context 'with pending accounts' do - before { Fabricate(:account) } - - it 'reports about pending accounts' do - expect { subject } - .to output_results( - 'already enabled', - 'still pending deletion' - ) - .and raise_error(SystemExit) - end - end - - context 'with sidekiq notices being processed' do - before do - Account.delete_all - stats_double = instance_double(Sidekiq::Stats, enqueued: 5) - allow(Sidekiq::Stats).to receive(:new).and_return(stats_double) - end - - it 'reports about notices' do - expect { subject } - .to output_results( - 'already enabled', - 'notices are still being' - ) - .and raise_error(SystemExit) - end - end - - context 'with sidekiq failed deliveries' do - before do - Account.delete_all - stats_double = instance_double(Sidekiq::Stats, enqueued: 0, retry_size: 10) - allow(Sidekiq::Stats).to receive(:new).and_return(stats_double) - end - - it 'reports about notices' do - expect { subject } - .to output_results( - 'already enabled', - 'some have failed and are scheduled' - ) - .and raise_error(SystemExit) - end - end - - context 'with self descruct mode ready' do - before do - Account.delete_all - stats_double = instance_double(Sidekiq::Stats, enqueued: 0, retry_size: 0) - allow(Sidekiq::Stats).to receive(:new).and_return(stats_double) - end - - it 'reports about notices' do - expect { subject } - .to output_results( - 'already enabled', - 'can safely delete all data' - ) - .and raise_error(SystemExit) - end - end - end - - context 'with self destruct mode disabled' do - before do - allow(SelfDestructHelper).to receive(:self_destruct?).and_return(false) - end - - context 'with an incorrect response to hostname' do - before do - answer_hostname_incorrectly - end - - it 'exits with mismatch error message' do - expect { subject } - .to raise_error(Thor::Error, /Domains do not match/) - end - end - - context 'with a correct response to hostname but no to proceed' do - before do - answer_hostname_correctly - decline_proceed - end - - it 'passes first step but stops before instructions' do - expect { subject } - .to output_results('operation WILL NOT') - .and raise_error(Thor::Error, /Self-destruct will not begin/) - end - end - - context 'with a correct response to hostname and yes to proceed' do - before do - answer_hostname_correctly - accept_proceed - end - - it 'instructs to set the appropriate environment variable' do - expect { subject } - .to output_results( - 'operation WILL NOT', - 'the following variable' - ) - end - end - - private - - def answer_hostname_incorrectly - allow(cli.shell) - .to receive(:ask) - .with('Type in the domain of the server to confirm:') - .and_return('wrong.host') - .once - end - - def answer_hostname_correctly - allow(cli.shell) - .to receive(:ask) - .with('Type in the domain of the server to confirm:') - .and_return(Rails.configuration.x.local_domain) - .once - end - - def decline_proceed - allow(cli.shell) - .to receive(:no?) - .with('Are you sure you want to proceed?') - .and_return(true) - .once - end - - def accept_proceed - allow(cli.shell) - .to receive(:no?) - .with('Are you sure you want to proceed?') - .and_return(false) - .once - end - end - end -end diff --git a/spec/lib/mastodon/cli/maintenance_spec.rb b/spec/lib/mastodon/cli/maintenance_spec.rb deleted file mode 100644 index cde25d39ed..0000000000 --- a/spec/lib/mastodon/cli/maintenance_spec.rb +++ /dev/null @@ -1,603 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/maintenance' - -describe Mastodon::CLI::Maintenance do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#fix_duplicates' do - let(:action) { :fix_duplicates } - - context 'when the database version is too old' do - before do - allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2000_01_01_000000) # Earlier than minimum - end - - it 'Exits with error message' do - expect { subject } - .to raise_error(Thor::Error, /is too old/) - end - end - - context 'when the database version is too new and the user does not continue' do - before do - allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2100_01_01_000000) # Later than maximum - allow(cli.shell).to receive(:yes?).with('Continue anyway? (Yes/No)').and_return(false).once - end - - it 'Exits with error message' do - expect { subject } - .to output_results('more recent') - .and raise_error(Thor::Error, /more recent/) - end - end - - context 'when Sidekiq is running' do - before do - allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2022_01_01_000000) # Higher than minimum, lower than maximum - allow(Sidekiq::ProcessSet).to receive(:new).and_return [:process] - end - - it 'Exits with error message' do - expect { subject } - .to raise_error(Thor::Error, /Sidekiq is running/) - end - end - - context 'when requirements are met' do - before do - allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2023_08_22_081029) # The latest migration before the cutoff - agree_to_backup_warning - end - - context 'with duplicate accounts' do - before do - prepare_duplicate_data - choose_local_account_to_keep - end - - let(:duplicate_account_username) { 'username' } - let(:duplicate_account_domain) { 'host.example' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating accounts', - 'Multiple local accounts were found for', - 'Restoring index_accounts_on_username_and_domain_lower', - 'Reindexing textual indexes on accounts…', - 'Finished!' - ) - .and change(duplicate_remote_accounts, :count).from(2).to(1) - .and change(duplicate_local_accounts, :count).from(2).to(1) - end - - def duplicate_remote_accounts - Account.where(username: duplicate_account_username, domain: duplicate_account_domain) - end - - def duplicate_local_accounts - Account.where(username: duplicate_account_username, domain: nil) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :accounts, name: :index_accounts_on_username_and_domain_lower - _remote_account = Fabricate(:account, username: duplicate_account_username, domain: duplicate_account_domain) - _remote_account_dupe = Fabricate.build(:account, username: duplicate_account_username, domain: duplicate_account_domain).save(validate: false) - _local_account = Fabricate(:account, username: duplicate_account_username, domain: nil) - _local_account_dupe = Fabricate.build(:account, username: duplicate_account_username, domain: nil).save(validate: false) - end - - def choose_local_account_to_keep - allow(cli.shell) - .to receive(:ask) - .with(/Account to keep unchanged/, anything) - .and_return('0') - .once - end - end - - context 'with duplicate users on email' do - before do - prepare_duplicate_data - end - - let(:duplicate_email) { 'duplicate@example.host' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating user records', - 'Restoring users indexes', - 'Finished!' - ) - .and change(duplicate_users, :count).from(2).to(1) - end - - def duplicate_users - User.where(email: duplicate_email) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :users, :email - Fabricate(:user, email: duplicate_email) - Fabricate.build(:user, email: duplicate_email).save(validate: false) - end - end - - context 'with duplicate users on confirmation_token' do - before do - prepare_duplicate_data - end - - let(:duplicate_confirmation_token) { '123ABC' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating user records', - 'Unsetting confirmation token', - 'Restoring users indexes', - 'Finished!' - ) - .and change(duplicate_users, :count).from(2).to(1) - end - - def duplicate_users - User.where(confirmation_token: duplicate_confirmation_token) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :users, :confirmation_token - Fabricate(:user, confirmation_token: duplicate_confirmation_token) - Fabricate.build(:user, confirmation_token: duplicate_confirmation_token).save(validate: false) - end - end - - context 'with duplicate users on reset_password_token' do - before do - prepare_duplicate_data - end - - let(:duplicate_reset_password_token) { '123ABC' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating user records', - 'Unsetting password reset token', - 'Restoring users indexes', - 'Finished!' - ) - .and change(duplicate_users, :count).from(2).to(1) - end - - def duplicate_users - User.where(reset_password_token: duplicate_reset_password_token) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :users, :reset_password_token - Fabricate(:user, reset_password_token: duplicate_reset_password_token) - Fabricate.build(:user, reset_password_token: duplicate_reset_password_token).save(validate: false) - end - end - - context 'with duplicate account_domain_blocks' do - before do - prepare_duplicate_data - end - - let(:duplicate_domain) { 'example.host' } - let(:account) { Fabricate(:account) } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Removing duplicate account domain blocks', - 'Restoring account domain blocks indexes', - 'Finished!' - ) - .and change(duplicate_account_domain_blocks, :count).from(2).to(1) - end - - def duplicate_account_domain_blocks - AccountDomainBlock.where(account: account, domain: duplicate_domain) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :account_domain_blocks, [:account_id, :domain] - Fabricate(:account_domain_block, account: account, domain: duplicate_domain) - Fabricate.build(:account_domain_block, account: account, domain: duplicate_domain).save(validate: false) - end - end - - context 'with duplicate announcement_reactions' do - before do - prepare_duplicate_data - end - - let(:account) { Fabricate(:account) } - let(:announcement) { Fabricate(:announcement) } - let(:name) { Fabricate(:custom_emoji).shortcode } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Removing duplicate announcement reactions', - 'Restoring announcement_reactions indexes', - 'Finished!' - ) - .and change(duplicate_announcement_reactions, :count).from(2).to(1) - end - - def duplicate_announcement_reactions - AnnouncementReaction.where(account: account, announcement: announcement, name: name) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :announcement_reactions, [:account_id, :announcement_id, :name] - Fabricate(:announcement_reaction, account: account, announcement: announcement, name: name) - Fabricate.build(:announcement_reaction, account: account, announcement: announcement, name: name).save(validate: false) - end - end - - context 'with duplicate conversations' do - before do - prepare_duplicate_data - end - - let(:uri) { 'https://example.host/path' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating conversations', - 'Restoring conversations indexes', - 'Finished!' - ) - .and change(duplicate_conversations, :count).from(2).to(1) - end - - def duplicate_conversations - Conversation.where(uri: uri) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :conversations, :uri - Fabricate(:conversation, uri: uri) - Fabricate.build(:conversation, uri: uri).save(validate: false) - end - end - - context 'with duplicate custom_emojis' do - before do - prepare_duplicate_data - end - - let(:duplicate_shortcode) { 'wowzers' } - let(:duplicate_domain) { 'example.host' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating custom_emojis', - 'Restoring custom_emojis indexes', - 'Finished!' - ) - .and change(duplicate_custom_emojis, :count).from(2).to(1) - end - - def duplicate_custom_emojis - CustomEmoji.where(shortcode: duplicate_shortcode, domain: duplicate_domain) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :custom_emojis, [:shortcode, :domain] - Fabricate(:custom_emoji, shortcode: duplicate_shortcode, domain: duplicate_domain) - Fabricate.build(:custom_emoji, shortcode: duplicate_shortcode, domain: duplicate_domain).save(validate: false) - end - end - - context 'with duplicate custom_emoji_categories' do - before do - prepare_duplicate_data - end - - let(:duplicate_name) { 'name_value' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating custom_emoji_categories', - 'Restoring custom_emoji_categories indexes', - 'Finished!' - ) - .and change(duplicate_custom_emoji_categories, :count).from(2).to(1) - end - - def duplicate_custom_emoji_categories - CustomEmojiCategory.where(name: duplicate_name) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :custom_emoji_categories, :name - Fabricate(:custom_emoji_category, name: duplicate_name) - Fabricate.build(:custom_emoji_category, name: duplicate_name).save(validate: false) - end - end - - context 'with duplicate domain_allows' do - before do - prepare_duplicate_data - end - - let(:domain) { 'example.host' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating domain_allows', - 'Restoring domain_allows indexes', - 'Finished!' - ) - .and change(duplicate_domain_allows, :count).from(2).to(1) - end - - def duplicate_domain_allows - DomainAllow.where(domain: domain) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :domain_allows, :domain - Fabricate(:domain_allow, domain: domain) - Fabricate.build(:domain_allow, domain: domain).save(validate: false) - end - end - - context 'with duplicate domain_blocks' do - before do - prepare_duplicate_data - end - - let(:domain) { 'example.host' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating domain_blocks', - 'Restoring domain_blocks indexes', - 'Finished!' - ) - .and change(duplicate_domain_blocks, :count).from(2).to(1) - end - - def duplicate_domain_blocks - DomainBlock.where(domain: domain) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :domain_blocks, :domain - Fabricate(:domain_block, domain: domain) - Fabricate.build(:domain_block, domain: domain).save(validate: false) - end - end - - context 'with duplicate email_domain_blocks' do - before do - prepare_duplicate_data - end - - let(:domain) { 'example.host' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating email_domain_blocks', - 'Restoring email_domain_blocks indexes', - 'Finished!' - ) - .and change(duplicate_email_domain_blocks, :count).from(2).to(1) - end - - def duplicate_email_domain_blocks - EmailDomainBlock.where(domain: domain) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :email_domain_blocks, :domain - Fabricate(:email_domain_block, domain: domain) - Fabricate.build(:email_domain_block, domain: domain).save(validate: false) - end - end - - context 'with duplicate media_attachments' do - before do - prepare_duplicate_data - end - - let(:shortcode) { 'codenam' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating media_attachments', - 'Restoring media_attachments indexes', - 'Finished!' - ) - .and change(duplicate_media_attachments, :count).from(2).to(1) - end - - def duplicate_media_attachments - MediaAttachment.where(shortcode: shortcode) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :media_attachments, :shortcode - Fabricate(:media_attachment, shortcode: shortcode) - Fabricate.build(:media_attachment, shortcode: shortcode).save(validate: false) - end - end - - context 'with duplicate preview_cards' do - before do - prepare_duplicate_data - end - - let(:url) { 'https://example.host/path' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating preview_cards', - 'Restoring preview_cards indexes', - 'Finished!' - ) - .and change(duplicate_preview_cards, :count).from(2).to(1) - end - - def duplicate_preview_cards - PreviewCard.where(url: url) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :preview_cards, :url - Fabricate(:preview_card, url: url) - Fabricate.build(:preview_card, url: url).save(validate: false) - end - end - - context 'with duplicate statuses' do - before do - prepare_duplicate_data - end - - let(:uri) { 'https://example.host/path' } - let(:account) { Fabricate(:account) } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating statuses', - 'Restoring statuses indexes', - 'Finished!' - ) - .and change(duplicate_statuses, :count).from(2).to(1) - end - - def duplicate_statuses - Status.where(uri: uri) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :statuses, :uri - Fabricate(:status, account: account, uri: uri) - duplicate = Fabricate.build(:status, account: account, uri: uri) - duplicate.save(validate: false) - Fabricate(:status_pin, account: account, status: duplicate) - Fabricate(:status, in_reply_to_id: duplicate.id) - Fabricate(:status, reblog_of_id: duplicate.id) - end - end - - context 'with duplicate tags' do - before do - prepare_duplicate_data - end - - let(:name) { 'tagname' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating tags', - 'Restoring tags indexes', - 'Finished!' - ) - .and change(duplicate_tags, :count).from(2).to(1) - end - - def duplicate_tags - Tag.where(name: name) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :tags, name: 'index_tags_on_name_lower_btree' - Fabricate(:tag, name: name) - Fabricate.build(:tag, name: name).save(validate: false) - end - end - - context 'with duplicate webauthn_credentials' do - before do - prepare_duplicate_data - end - - let(:external_id) { '123_123_123' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating webauthn_credentials', - 'Restoring webauthn_credentials indexes', - 'Finished!' - ) - .and change(duplicate_webauthn_credentials, :count).from(2).to(1) - end - - def duplicate_webauthn_credentials - WebauthnCredential.where(external_id: external_id) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :webauthn_credentials, :external_id - Fabricate(:webauthn_credential, external_id: external_id) - Fabricate.build(:webauthn_credential, external_id: external_id).save(validate: false) - end - end - - context 'with duplicate webhooks' do - before do - prepare_duplicate_data - end - - let(:url) { 'https://example.host/path' } - - it 'runs the deduplication process' do - expect { subject } - .to output_results( - 'Deduplicating webhooks', - 'Restoring webhooks indexes', - 'Finished!' - ) - .and change(duplicate_webhooks, :count).from(2).to(1) - end - - def duplicate_webhooks - Webhook.where(url: url) - end - - def prepare_duplicate_data - ActiveRecord::Base.connection.remove_index :webhooks, :url - Fabricate(:webhook, url: url) - Fabricate.build(:webhook, url: url).save(validate: false) - end - end - - def agree_to_backup_warning - allow(cli.shell) - .to receive(:yes?) - .with('Continue? (Yes/No)') - .and_return(true) - .once - end - end - end -end diff --git a/spec/lib/mastodon/cli/media_spec.rb b/spec/lib/mastodon/cli/media_spec.rb deleted file mode 100644 index ecc7101b6c..0000000000 --- a/spec/lib/mastodon/cli/media_spec.rb +++ /dev/null @@ -1,249 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/media' - -describe Mastodon::CLI::Media do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#remove' do - let(:action) { :remove } - - context 'with --prune-profiles and --remove-headers' do - let(:options) { { prune_profiles: true, remove_headers: true } } - - it 'warns about usage and exits' do - expect { subject } - .to raise_error(Thor::Error, '--prune-profiles and --remove-headers should not be specified simultaneously') - end - end - - context 'with --include-follows but not including --prune-profiles and --remove-headers' do - let(:options) { { include_follows: true } } - - it 'warns about usage and exits' do - expect { subject } - .to raise_error(Thor::Error, '--include-follows can only be used with --prune-profiles or --remove-headers') - end - end - - context 'with a relevant account' do - let!(:account) do - Fabricate(:account, domain: 'example.com', updated_at: 1.month.ago, last_webfingered_at: 1.month.ago, avatar: attachment_fixture('attachment.jpg'), header: attachment_fixture('attachment.jpg')) - end - - context 'with --prune-profiles' do - let(:options) { { prune_profiles: true } } - - it 'removes account avatars' do - expect { subject } - .to output_results('Visited 1') - - expect(account.reload.avatar).to be_blank - end - end - - context 'with --remove-headers' do - let(:options) { { remove_headers: true } } - - it 'removes account header' do - expect { subject } - .to output_results('Visited 1') - - expect(account.reload.header).to be_blank - end - end - end - - context 'with a relevant media attachment' do - let!(:media_attachment) { Fabricate(:media_attachment, remote_url: 'https://example.com/image.jpg', created_at: 1.month.ago) } - - context 'without options' do - it 'removes account avatars' do - expect { subject } - .to output_results('Removed 1') - - expect(media_attachment.reload.file).to be_blank - expect(media_attachment.reload.thumbnail).to be_blank - end - end - end - end - - describe '#usage' do - let(:action) { :usage } - - context 'without options' do - it 'reports about storage size' do - expect { subject } - .to output_results('0 Bytes') - end - end - end - - describe '#lookup' do - let(:action) { :lookup } - let(:arguments) { [url] } - - context 'with valid url not connected to a record' do - let(:url) { 'https://example.host/assets/1' } - - it 'warns about url and exits' do - expect { subject } - .to raise_error(Thor::Error, 'Not a media URL') - end - end - - context 'with a valid media url' do - let(:status) { Fabricate(:status) } - let(:media_attachment) { Fabricate(:media_attachment, status: status) } - let(:url) { media_attachment.file.url(:original) } - - it 'displays the url of a connected status' do - expect { subject } - .to output_results(status.id.to_s) - end - end - end - - describe '#refresh' do - let(:action) { :refresh } - - context 'without any options' do - it 'warns about usage and exits' do - expect { subject } - .to raise_error(Thor::Error, /Specify the source/) - end - end - - context 'with --status option' do - before do - media_attachment.update(file_file_name: nil) - end - - let(:media_attachment) { Fabricate(:media_attachment, status: status, remote_url: 'https://host.example/asset.jpg') } - let(:options) { { status: status.id } } - let(:status) { Fabricate(:status) } - - it 'redownloads the attachment file' do - expect { subject } - .to output_results('Downloaded 1 media') - end - end - - context 'with --account option' do - context 'when the account does not exist' do - let(:options) { { account: 'not-real-user@example.host' } } - - it 'warns about usage and exits' do - expect { subject } - .to raise_error(Thor::Error, 'No such account') - end - end - - context 'when the account exists' do - before do - media_attachment.update(file_file_name: nil) - end - - let(:media_attachment) { Fabricate(:media_attachment, account: account) } - let(:options) { { account: account.acct } } - let(:account) { Fabricate(:account) } - - it 'redownloads the attachment file' do - expect { subject } - .to output_results('Downloaded 1 media') - end - end - end - - context 'with --domain option' do - before do - media_attachment.update(file_file_name: nil) - end - - let(:domain) { 'example.host' } - let(:media_attachment) { Fabricate(:media_attachment, account: account) } - let(:options) { { domain: domain } } - let(:account) { Fabricate(:account, domain: domain) } - - it 'redownloads the attachment file' do - expect { subject } - .to output_results('Downloaded 1 media') - end - end - - context 'with --days option' do - before do - Fabricate(:media_attachment, remote_url: 'https://example.com/image.jpg', id: Mastodon::Snowflake.id_at(50.days.ago)) - Fabricate(:media_attachment, remote_url: 'https://example.com/image.jpg', id: Mastodon::Snowflake.id_at(5.days.ago)) - Fabricate(:media_attachment, remote_url: '', id: Mastodon::Snowflake.id_at(5.days.ago)) - end - - let(:options) { { days: 10 } } - - it 'redownloads the attachment file for the remote records more recent than the option' do - expect { subject } - .to output_results('Downloaded 1 media') - end - end - end - - describe '#remove_orphans' do - let(:action) { :remove_orphans } - - before do - FileUtils.mkdir_p Rails.public_path.join('system') - end - - context 'without any options' do - it 'runs without error' do - expect { subject } - .to output_results('Removed', 'orphans (approx') - end - end - - context 'when in azure mode' do - before do - allow(Paperclip::Attachment).to receive(:default_options).and_return(storage: :azure) - end - - it 'warns about usage and exits' do - expect { subject } - .to raise_error(Thor::Error, /azure storage driver is not supported/) - end - end - - context 'when in fog mode' do - before do - allow(Paperclip::Attachment).to receive(:default_options).and_return(storage: :fog) - end - - it 'warns about usage and exits' do - expect { subject } - .to raise_error(Thor::Error, /fog storage driver is not supported/) - end - end - - context 'when in filesystem mode' do - before do - allow(File).to receive(:delete).and_return(true) - media_attachment.delete - end - - let(:media_attachment) { Fabricate(:media_attachment) } - - it 'removes the unlinked files' do - expect { subject } - .to output_results('Removed', 'orphans (approx') - expect(File).to have_received(:delete).with(media_attachment.file.path) - end - end - end -end diff --git a/spec/lib/mastodon/cli/preview_cards_spec.rb b/spec/lib/mastodon/cli/preview_cards_spec.rb deleted file mode 100644 index 951ae3758f..0000000000 --- a/spec/lib/mastodon/cli/preview_cards_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/preview_cards' - -describe Mastodon::CLI::PreviewCards do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#remove' do - let(:action) { :remove } - - context 'with relevant preview cards' do - before do - Fabricate(:preview_card, updated_at: 10.years.ago, type: :link) - Fabricate(:preview_card, updated_at: 10.months.ago, type: :photo) - Fabricate(:preview_card, updated_at: 10.days.ago, type: :photo) - end - - context 'with no arguments' do - it 'deletes thumbnails for local preview cards' do - expect { subject } - .to output_results( - 'Removed 2 preview cards', - 'approx. 119 KB' - ) - end - end - - context 'with the --link option' do - let(:options) { { link: true } } - - it 'deletes thumbnails for local preview cards' do - expect { subject } - .to output_results( - 'Removed 1 link-type preview cards', - 'approx. 59.6 KB' - ) - end - end - - context 'with the --days option' do - let(:options) { { days: 365 } } - - it 'deletes thumbnails for local preview cards' do - expect { subject } - .to output_results( - 'Removed 1 preview cards', - 'approx. 59.6 KB' - ) - end - end - end - end -end diff --git a/spec/lib/mastodon/cli/search_spec.rb b/spec/lib/mastodon/cli/search_spec.rb deleted file mode 100644 index ed3789c3e7..0000000000 --- a/spec/lib/mastodon/cli/search_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/search' - -describe Mastodon::CLI::Search do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#deploy' do - let(:action) { :deploy } - - context 'with concurrency out of range' do - let(:options) { { concurrency: -100 } } - - it 'Exits with error message' do - expect { subject } - .to raise_error(Thor::Error, /this concurrency setting/) - end - end - - context 'with batch size out of range' do - let(:options) { { batch_size: -100_000 } } - - it 'Exits with error message' do - expect { subject } - .to raise_error(Thor::Error, /this batch_size setting/) - end - end - - context 'when server communication raises an error' do - let(:options) { { reset_chewy: true } } - - before { allow(Chewy::Stash::Specification).to receive(:reset!).and_raise(Elasticsearch::Transport::Transport::Errors::InternalServerError) } - - it 'Exits with error message' do - expect { subject } - .to raise_error(Thor::Error, /issue connecting to the search/) - end - end - - context 'without options' do - before { stub_search_indexes } - - let(:indexed_count) { 1 } - let(:deleted_count) { 2 } - - it 'reports about storage size' do - expect { subject } - .to output_results( - "Indexed #{described_class::INDICES.size * indexed_count} records", - "de-indexed #{described_class::INDICES.size * deleted_count}" - ) - end - end - - def stub_search_indexes - described_class::INDICES.each do |index| - allow(index) - .to receive_messages( - specification: instance_double(Chewy::Index::Specification, changed?: true, lock!: nil), - purge: nil - ) - - importer_double = importer_double_for(index) - allow(importer_double).to receive(:on_progress).and_yield([indexed_count, deleted_count]) - allow("Importer::#{index}Importer".constantize) - .to receive(:new) - .and_return(importer_double) - end - end - - def importer_double_for(index) - instance_double( - "Importer::#{index}Importer".constantize, - clean_up!: nil, - estimate!: 100, - import!: nil, - on_failure: nil, - # on_progress: nil, - optimize_for_import!: nil, - optimize_for_search!: nil - ) - end - end -end diff --git a/spec/lib/mastodon/cli/settings_spec.rb b/spec/lib/mastodon/cli/settings_spec.rb deleted file mode 100644 index e1b353eb90..0000000000 --- a/spec/lib/mastodon/cli/settings_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/settings' - -describe Mastodon::CLI::Settings do - it_behaves_like 'CLI Command' - - describe 'subcommand "registrations"' do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { Mastodon::CLI::Registrations.new } - let(:arguments) { [] } - let(:options) { {} } - - before do - Setting.registrations_mode = nil - end - - describe '#open' do - let(:action) { :open } - - it 'changes "registrations_mode" to "open" and displays success' do - expect { subject } - .to change(Setting, :registrations_mode).from(nil).to('open') - .and output_results('OK') - end - end - - describe '#approved' do - let(:action) { :approved } - - it 'changes "registrations_mode" to "approved" and displays success' do - expect { subject } - .to change(Setting, :registrations_mode).from(nil).to('approved') - .and output_results('OK') - end - - context 'with --require-reason' do - let(:options) { { require_reason: true } } - - it 'changes registrations_mode and require_invite_text' do - expect { subject } - .to output_results('OK') - .and change(Setting, :registrations_mode).from(nil).to('approved') - .and change(Setting, :require_invite_text).from(false).to(true) - end - end - end - - describe '#close' do - let(:action) { :close } - - it 'changes "registrations_mode" to "none" and displays success' do - expect { subject } - .to change(Setting, :registrations_mode).from(nil).to('none') - .and output_results('OK') - end - end - end -end diff --git a/spec/lib/mastodon/cli/statuses_spec.rb b/spec/lib/mastodon/cli/statuses_spec.rb deleted file mode 100644 index 161b7c02bb..0000000000 --- a/spec/lib/mastodon/cli/statuses_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/statuses' - -describe Mastodon::CLI::Statuses do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#remove', use_transactional_tests: false do - let(:action) { :remove } - - context 'with small batch size' do - let(:options) { { batch_size: 0 } } - - it 'exits with error message' do - expect { subject } - .to raise_error(Thor::Error, /Cannot run/) - end - end - - context 'with default batch size' do - it 'removes unreferenced statuses' do - expect { subject } - .to output_results('Done after') - end - end - end -end diff --git a/spec/lib/mastodon/cli/upgrade_spec.rb b/spec/lib/mastodon/cli/upgrade_spec.rb deleted file mode 100644 index 6861e04887..0000000000 --- a/spec/lib/mastodon/cli/upgrade_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/cli/upgrade' - -describe Mastodon::CLI::Upgrade do - subject { cli.invoke(action, arguments, options) } - - let(:cli) { described_class.new } - let(:arguments) { [] } - let(:options) { {} } - - it_behaves_like 'CLI Command' - - describe '#storage_schema' do - let(:action) { :storage_schema } - - context 'with records that dont need upgrading' do - before do - Fabricate(:account) - Fabricate(:media_attachment) - end - - it 'does not upgrade storage for the attachments' do - expect { subject } - .to output_results('Upgraded storage schema of 0 records') - end - end - end -end diff --git a/spec/lib/mastodon/migration_warning_spec.rb b/spec/lib/mastodon/migration_warning_spec.rb deleted file mode 100644 index 4adf0837ab..0000000000 --- a/spec/lib/mastodon/migration_warning_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'mastodon/migration_warning' - -describe Mastodon::MigrationWarning do - describe 'migration_duration_warning' do - before do - allow(migration).to receive(:valid_environment?).and_return(true) - allow(migration).to receive(:sleep).with(1) - end - - let(:migration) { Class.new(ActiveRecord::Migration[6.1]).extend(described_class) } - - context 'with the default message' do - it 'warns about long migrations' do - expectation = expect { migration.migration_duration_warning } - - expectation.to output(/interrupt this migration/).to_stdout - expectation.to output(/Continuing in 5/).to_stdout - end - end - - context 'with an additional message' do - it 'warns about long migrations' do - expectation = expect { migration.migration_duration_warning('Get ready for it') } - - expectation.to output(/interrupt this migration/).to_stdout - expectation.to output(/Get ready for it/).to_stdout - expectation.to output(/Continuing in 5/).to_stdout - end - end - end -end diff --git a/spec/lib/ostatus/tag_manager_spec.rb b/spec/lib/ostatus/tag_manager_spec.rb deleted file mode 100644 index 0e20f26c7c..0000000000 --- a/spec/lib/ostatus/tag_manager_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe OStatus::TagManager do - describe '#unique_tag' do - it 'returns a unique tag' do - expect(described_class.instance.unique_tag(Time.utc(2000), 12, 'Status')).to eq 'tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status' - end - end - - describe '#unique_tag_to_local_id' do - it 'returns the ID part' do - expect(described_class.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status', 'Status')).to eql '12' - end - - it 'returns nil if it is not local id' do - expect(described_class.instance.unique_tag_to_local_id('tag:remote,2000-01-01:objectId=12:objectType=Status', 'Status')).to be_nil - end - - it 'returns nil if it is not expected type' do - expect(described_class.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Block', 'Status')).to be_nil - end - - it 'returns nil if it does not have object ID' do - expect(described_class.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectType=Status', 'Status')).to be_nil - end - end - - describe '#local_id?' do - it 'returns true for a local ID' do - expect(described_class.instance.local_id?('tag:cb6e6126.ngrok.io;objectId=12:objectType=Status')).to be true - end - - it 'returns false for a foreign ID' do - expect(described_class.instance.local_id?('tag:foreign.tld;objectId=12:objectType=Status')).to be false - end - end - - describe '#uri_for' do - subject { described_class.instance.uri_for(target) } - - context 'with comment object' do - let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) } - - it 'returns the unique tag for status' do - expect(target.object_type).to eq :comment - expect(subject).to eq target.uri - end - end - - context 'with note object' do - let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: false, thread: nil) } - - it 'returns the unique tag for status' do - expect(target.object_type).to eq :note - expect(subject).to eq target.uri - end - end - - context 'when person object' do - let(:target) { Fabricate(:account, username: 'alice') } - - it 'returns the URL for account' do - expect(target.object_type).to eq :person - expect(subject).to eq 'https://cb6e6126.ngrok.io/users/alice' - end - end - end -end diff --git a/spec/lib/permalink_redirector_spec.rb b/spec/lib/permalink_redirector_spec.rb deleted file mode 100644 index a009136561..0000000000 --- a/spec/lib/permalink_redirector_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe PermalinkRedirector do - let(:remote_account) { Fabricate(:account, username: 'alice', domain: 'example.com', url: 'https://example.com/@alice', id: 2) } - - describe '#redirect_url' do - before do - Fabricate(:status, account: remote_account, id: 123, url: 'https://example.com/status-123') - end - - it 'returns path for legacy account links' do - redirector = described_class.new('accounts/2') - expect(redirector.redirect_path).to eq 'https://example.com/@alice' - end - - it 'returns path for legacy status links' do - redirector = described_class.new('statuses/123') - expect(redirector.redirect_path).to eq 'https://example.com/status-123' - end - - it 'returns path for pretty account links' do - redirector = described_class.new('@alice@example.com') - expect(redirector.redirect_path).to eq 'https://example.com/@alice' - end - - it 'returns path for pretty status links' do - redirector = described_class.new('@alice/123') - expect(redirector.redirect_path).to eq 'https://example.com/status-123' - end - end -end diff --git a/spec/lib/plain_text_formatter_spec.rb b/spec/lib/plain_text_formatter_spec.rb deleted file mode 100644 index b22f473d0c..0000000000 --- a/spec/lib/plain_text_formatter_spec.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe PlainTextFormatter do - describe '#to_s' do - subject { described_class.new(status.text, status.local?).to_s } - - context 'when status is local' do - let(:status) { Fabricate(:status, text: '

a text by a nerd who uses an HTML tag in text

', uri: nil) } - - it 'returns the raw text' do - expect(subject).to eq '

a text by a nerd who uses an HTML tag in text

' - end - end - - context 'when status is remote' do - let(:remote_account) { Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') } - - context 'when text contains inline HTML tags' do - let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem ipsum') } - - it 'strips the tags' do - expect(subject).to eq 'Lorem ipsum' - end - end - - context 'when text contains

tags' do - let(:status) { Fabricate(:status, account: remote_account, text: '

Lorem

ipsum

') } - - it 'inserts a newline' do - expect(subject).to eq "Lorem\nipsum" - end - end - - context 'when text contains a single
tag' do - let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem
ipsum') } - - it 'inserts a newline' do - expect(subject).to eq "Lorem\nipsum" - end - end - - context 'when text contains consecutive
tag' do - let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem


ipsum') } - - it 'inserts a single newline' do - expect(subject).to eq "Lorem\nipsum" - end - end - - context 'when text contains HTML entity' do - let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem & ipsum ❤') } - - it 'unescapes the entity' do - expect(subject).to eq 'Lorem & ipsum ❤' - end - end - - context 'when text contains ipsum') } - - it 'strips the tag and its contents' do - expect(subject).to eq 'Lorem ipsum' - end - end - - context 'when text contains an HTML comment tags' do - let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem ipsum') } - - it 'strips the comment' do - expect(subject).to eq 'Lorem ipsum' - end - end - - context 'when text contains HTML ruby tags' do - let(:status) { Fabricate(:status, account: remote_account, text: '

Lorem 明日 (Ashita) ipsum

') } - - it 'strips the comment' do - expect(subject).to eq 'Lorem 明日 (Ashita) ipsum' - end - end - end - end -end diff --git a/spec/lib/request_pool_spec.rb b/spec/lib/request_pool_spec.rb deleted file mode 100644 index a82eb5a188..0000000000 --- a/spec/lib/request_pool_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe RequestPool do - subject { described_class.new } - - describe '#with' do - it 'returns a HTTP client for a host' do - subject.with('http://example.com') do |http_client| - expect(http_client).to be_a HTTP::Client - end - end - - it 'returns the same instance of HTTP client within the same thread for the same host' do - test_client = nil - - subject.with('http://example.com') { |http_client| test_client = http_client } - expect(test_client).to_not be_nil - subject.with('http://example.com') { |http_client| expect(http_client).to be test_client } - end - - it 'returns different HTTP clients for different hosts' do - test_client = nil - - subject.with('http://example.com') { |http_client| test_client = http_client } - expect(test_client).to_not be_nil - subject.with('http://example.org') { |http_client| expect(http_client).to_not be test_client } - end - - it 'grows to the number of threads accessing it' do - stub_request(:get, 'http://example.com/').to_return(status: 200, body: 'Hello!') - - subject - - multi_threaded_execution(5) do - subject.with('http://example.com') do |http_client| - http_client.get('/').flush - # Nudge scheduler to yield and exercise the full pool - sleep(0.01) - end - end - - expect(subject.size).to be > 1 - end - - context 'with an idle connection' do - before do - stub_const('RequestPool::MAX_IDLE_TIME', 1) # Lower idle time limit to 1 seconds - stub_const('RequestPool::REAPER_FREQUENCY', 0.1) # Run reaper every 0.1 seconds - stub_request(:get, 'http://example.com/').to_return(status: 200, body: 'Hello!') - end - - it 'closes the connections' do - subject.with('http://example.com') do |http_client| - http_client.get('/').flush - end - - expect { reaper_observes_idle_timeout }.to change(subject, :size).from(1).to(0) - end - - def reaper_observes_idle_timeout - # One full idle period and 2 reaper cycles more - sleep RequestPool::MAX_IDLE_TIME + (RequestPool::REAPER_FREQUENCY * 2) - end - end - end -end diff --git a/spec/lib/request_spec.rb b/spec/lib/request_spec.rb deleted file mode 100644 index c7620cf9b6..0000000000 --- a/spec/lib/request_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'securerandom' - -describe Request do - subject { described_class.new(:get, 'http://example.com') } - - describe '#headers' do - it 'returns user agent' do - expect(subject.headers['User-Agent']).to be_present - end - - it 'returns the date header' do - expect(subject.headers['Date']).to be_present - end - - it 'returns the host header' do - expect(subject.headers['Host']).to be_present - end - - it 'does not return virtual request-target header' do - expect(subject.headers['(request-target)']).to be_nil - end - end - - describe '#on_behalf_of' do - it 'when used, adds signature header' do - subject.on_behalf_of(Fabricate(:account)) - expect(subject.headers['Signature']).to be_present - end - end - - describe '#add_headers' do - it 'adds headers to the request' do - subject.add_headers('Test' => 'Foo') - expect(subject.headers['Test']).to eq 'Foo' - end - end - - describe '#perform' do - context 'with valid host' do - before { stub_request(:get, 'http://example.com') } - - it 'executes a HTTP request' do - expect { |block| subject.perform(&block) }.to yield_control - expect(a_request(:get, 'http://example.com')).to have_been_made.once - end - - it 'executes a HTTP request when the first address is private' do - resolver = instance_double(Resolv::DNS) - - allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:4860:4860::8844)) - allow(resolver).to receive(:timeouts=).and_return(nil) - allow(Resolv::DNS).to receive(:open).and_yield(resolver) - - expect { |block| subject.perform(&block) }.to yield_control - expect(a_request(:get, 'http://example.com')).to have_been_made.once - end - - it 'sets headers' do - expect { |block| subject.perform(&block) }.to yield_control - expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made - end - - it 'closes underlying connection' do - allow(subject.send(:http_client)).to receive(:close) - - expect { |block| subject.perform(&block) }.to yield_control - - expect(subject.send(:http_client)).to have_received(:close) - end - - it 'returns response which implements body_with_limit' do - subject.perform do |response| - expect(response).to respond_to :body_with_limit - end - end - end - - context 'with private host' do - around do |example| - WebMock.disable! - example.run - WebMock.enable! - end - - it 'raises Mastodon::ValidationError' do - resolver = instance_double(Resolv::DNS) - - allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:db8::face)) - allow(resolver).to receive(:timeouts=).and_return(nil) - allow(Resolv::DNS).to receive(:open).and_yield(resolver) - - expect { subject.perform }.to raise_error Mastodon::ValidationError - end - end - end - - describe "response's body_with_limit method" do - it 'rejects body more than 1 megabyte by default' do - stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes)) - expect { subject.perform(&:body_with_limit) }.to raise_error Mastodon::LengthValidationError - end - - it 'accepts body less than 1 megabyte by default' do - stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.kilobytes)) - expect { subject.perform(&:body_with_limit) }.to_not raise_error - end - - it 'rejects body by given size' do - stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.kilobytes)) - expect { subject.perform { |response| response.body_with_limit(1.kilobyte) } }.to raise_error Mastodon::LengthValidationError - end - - it 'rejects too large chunked body' do - stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Transfer-Encoding' => 'chunked' }) - expect { subject.perform(&:body_with_limit) }.to raise_error Mastodon::LengthValidationError - end - - it 'rejects too large monolithic body' do - stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Content-Length' => 2.megabytes }) - expect { subject.perform(&:body_with_limit) }.to raise_error Mastodon::LengthValidationError - end - - it 'truncates large monolithic body' do - stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Content-Length' => 2.megabytes }) - expect(subject.perform { |response| response.truncated_body.bytesize }).to be < 2.megabytes - end - - it 'uses binary encoding if Content-Type does not tell encoding' do - stub_request(:any, 'http://example.com').to_return(body: '', headers: { 'Content-Type' => 'text/html' }) - expect(subject.perform { |response| response.body_with_limit.encoding }).to eq Encoding::BINARY - end - - it 'uses binary encoding if Content-Type tells unknown encoding' do - stub_request(:any, 'http://example.com').to_return(body: '', headers: { 'Content-Type' => 'text/html; charset=unknown' }) - expect(subject.perform { |response| response.body_with_limit.encoding }).to eq Encoding::BINARY - end - - it 'uses encoding specified by Content-Type' do - stub_request(:any, 'http://example.com').to_return(body: '', headers: { 'Content-Type' => 'text/html; charset=UTF-8' }) - expect(subject.perform { |response| response.body_with_limit.encoding }).to eq Encoding::UTF_8 - end - end -end diff --git a/spec/lib/sanitize/config_spec.rb b/spec/lib/sanitize/config_spec.rb deleted file mode 100644 index a1e39153e6..0000000000 --- a/spec/lib/sanitize/config_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Sanitize::Config do - shared_examples 'common HTML sanitization' do - it 'keeps h1' do - expect(Sanitize.fragment('

Foo

', subject)).to eq '

Foo

' - end - - it 'keeps ul' do - expect(Sanitize.fragment('

Check out:

  • Foo
  • Bar
', subject)).to eq '

Check out:

  • Foo
  • Bar
' - end - - it 'keeps start and reversed attributes of ol' do - expect(Sanitize.fragment('

Check out:

  1. Foo
  2. Bar
', subject)).to eq '

Check out:

  1. Foo
  2. Bar
' - end - - it 'keeps ruby tags' do - expect(Sanitize.fragment('

明日 (Ashita)

', subject)).to eq '

明日 (Ashita)

' - end - - it 'removes a without href' do - expect(Sanitize.fragment('Test', subject)).to eq 'Test' - end - - it 'removes a without href and only keeps text content' do - expect(Sanitize.fragment('Test', subject)).to eq 'foo&Test' - end - - it 'removes a with unsupported scheme in href' do - expect(Sanitize.fragment('Test', subject)).to eq 'Test' - end - - it 'does not re-interpret HTML when removing unsupported links' do - expect(Sanitize.fragment('Test<a href="https://example.com">test</a>', subject)).to eq 'Test<a href="https://example.com">test</a>' - end - - it 'keeps a with href' do - expect(Sanitize.fragment('Test', subject)).to eq 'Test' - end - - it 'keeps a with translate="no"' do - expect(Sanitize.fragment('Test', subject)).to eq 'Test' - end - - it 'removes "translate" attribute with invalid value' do - expect(Sanitize.fragment('Test', subject)).to eq 'Test' - end - - it 'removes a with unparsable href' do - expect(Sanitize.fragment('Test', subject)).to eq 'Test' - end - - it 'keeps a with supported scheme and no host' do - expect(Sanitize.fragment('Test', subject)).to eq 'Test' - end - - it 'keeps title in abbr' do - expect(Sanitize.fragment('HTML', subject)).to eq 'HTML' - end - end - - describe '::MASTODON_OUTGOING' do - subject { described_class::MASTODON_OUTGOING } - - around do |example| - original_web_domain = Rails.configuration.x.web_domain - example.run - Rails.configuration.x.web_domain = original_web_domain - end - - it_behaves_like 'common HTML sanitization' - - it 'keeps a with href and rel tag, not adding to rel or target if url is local' do - Rails.configuration.x.web_domain = 'domain.test' - expect(Sanitize.fragment('', subject)).to eq '' - end - end -end diff --git a/spec/lib/scope_transformer_spec.rb b/spec/lib/scope_transformer_spec.rb deleted file mode 100644 index 7bc226e94f..0000000000 --- a/spec/lib/scope_transformer_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe ScopeTransformer do - describe '#apply' do - subject { described_class.new.apply(ScopeParser.new.parse(input)) } - - shared_examples 'a scope' do |namespace, term, access| - it 'parses the term' do - expect(subject.term).to eq term - end - - it 'parses the namespace' do - expect(subject.namespace).to eq namespace - end - - it 'parses the access' do - expect(subject.access).to eq access - end - end - - context 'with scope "profile"' do - let(:input) { 'profile' } - - it_behaves_like 'a scope', nil, 'profile', 'read' - end - - context 'with scope "read"' do - let(:input) { 'read' } - - it_behaves_like 'a scope', nil, 'all', 'read' - end - - context 'with scope "write"' do - let(:input) { 'write' } - - it_behaves_like 'a scope', nil, 'all', 'write' - end - - context 'with scope "follow"' do - let(:input) { 'follow' } - - it_behaves_like 'a scope', nil, 'follow', 'read/write' - end - - context 'with scope "crypto"' do - let(:input) { 'crypto' } - - it_behaves_like 'a scope', nil, 'crypto', 'read/write' - end - - context 'with scope "push"' do - let(:input) { 'push' } - - it_behaves_like 'a scope', nil, 'push', 'read/write' - end - - context 'with scope "admin:read"' do - let(:input) { 'admin:read' } - - it_behaves_like 'a scope', 'admin', 'all', 'read' - end - - context 'with scope "admin:write"' do - let(:input) { 'admin:write' } - - it_behaves_like 'a scope', 'admin', 'all', 'write' - end - - context 'with scope "admin:read:accounts"' do - let(:input) { 'admin:read:accounts' } - - it_behaves_like 'a scope', 'admin', 'accounts', 'read' - end - - context 'with scope "admin:write:accounts"' do - let(:input) { 'admin:write:accounts' } - - it_behaves_like 'a scope', 'admin', 'accounts', 'write' - end - - context 'with scope "read:accounts"' do - let(:input) { 'read:accounts' } - - it_behaves_like 'a scope', nil, 'accounts', 'read' - end - - context 'with scope "write:accounts"' do - let(:input) { 'write:accounts' } - - it_behaves_like 'a scope', nil, 'accounts', 'write' - end - end -end diff --git a/spec/lib/search_query_parser_spec.rb b/spec/lib/search_query_parser_spec.rb deleted file mode 100644 index 66b0e8f9e2..0000000000 --- a/spec/lib/search_query_parser_spec.rb +++ /dev/null @@ -1,98 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'parslet/rig/rspec' - -describe SearchQueryParser do - let(:parser) { described_class.new } - - context 'with term' do - it 'consumes "hello"' do - expect(parser.term).to parse('hello') - end - end - - context 'with prefix' do - it 'consumes "foo:"' do - expect(parser.prefix).to parse('foo:') - end - end - - context 'with operator' do - it 'consumes "+"' do - expect(parser.operator).to parse('+') - end - - it 'consumes "-"' do - expect(parser.operator).to parse('-') - end - end - - context 'with shortcode' do - it 'consumes ":foo:"' do - expect(parser.shortcode).to parse(':foo:') - end - end - - context 'with phrase' do - it 'consumes "hello world"' do - expect(parser.phrase).to parse('"hello world"') - end - end - - context 'with clause' do - it 'consumes "foo"' do - expect(parser.clause).to parse('foo') - end - - it 'consumes "-foo"' do - expect(parser.clause).to parse('-foo') - end - - it 'consumes "foo:bar"' do - expect(parser.clause).to parse('foo:bar') - end - - it 'consumes "-foo:bar"' do - expect(parser.clause).to parse('-foo:bar') - end - - it 'consumes \'foo:"hello world"\'' do - expect(parser.clause).to parse('foo:"hello world"') - end - - it 'consumes \'-foo:"hello world"\'' do - expect(parser.clause).to parse('-foo:"hello world"') - end - - it 'consumes "foo:"' do - expect(parser.clause).to parse('foo:') - end - - it 'consumes \'"\'' do - expect(parser.clause).to parse('"') - end - end - - context 'with query' do - it 'consumes "hello -world"' do - expect(parser.query).to parse('hello -world') - end - - it 'consumes \'foo "hello world"\'' do - expect(parser.query).to parse('foo "hello world"') - end - - it 'consumes "foo:bar hello"' do - expect(parser.query).to parse('foo:bar hello') - end - - it 'consumes \'"hello" world "\'' do - expect(parser.query).to parse('"hello" world "') - end - - it 'consumes "foo:bar bar: hello"' do - expect(parser.query).to parse('foo:bar bar: hello') - end - end -end diff --git a/spec/lib/search_query_transformer_spec.rb b/spec/lib/search_query_transformer_spec.rb deleted file mode 100644 index 5817e3d1d2..0000000000 --- a/spec/lib/search_query_transformer_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe SearchQueryTransformer do - subject { described_class.new.apply(parser, current_account: account) } - - let(:account) { Fabricate(:account) } - let(:parser) { SearchQueryParser.new.parse(query) } - - context 'with "hello world"' do - let(:query) { 'hello world' } - - it 'transforms clauses' do - expect(subject.send(:must_clauses).map(&:term)).to match_array %w(hello world) - expect(subject.send(:must_not_clauses)).to be_empty - expect(subject.send(:filter_clauses)).to be_empty - end - end - - context 'with "hello -world"' do - let(:query) { 'hello -world' } - - it 'transforms clauses' do - expect(subject.send(:must_clauses).map(&:term)).to match_array %w(hello) - expect(subject.send(:must_not_clauses).map(&:term)).to match_array %w(world) - expect(subject.send(:filter_clauses)).to be_empty - end - end - - context 'with "hello is:reply"' do - let(:query) { 'hello is:reply' } - - it 'transforms clauses' do - expect(subject.send(:must_clauses).map(&:term)).to match_array %w(hello) - expect(subject.send(:must_not_clauses)).to be_empty - expect(subject.send(:filter_clauses).map(&:term)).to match_array %w(reply) - end - end - - context 'with "foo: bar"' do - let(:query) { 'foo: bar' } - - it 'transforms clauses' do - expect(subject.send(:must_clauses).map(&:term)).to match_array %w(foo bar) - expect(subject.send(:must_not_clauses)).to be_empty - expect(subject.send(:filter_clauses)).to be_empty - end - end - - context 'with "foo:bar"' do - let(:query) { 'foo:bar' } - - it 'transforms clauses' do - expect(subject.send(:must_clauses).map(&:term)).to contain_exactly('foo bar') - expect(subject.send(:must_not_clauses)).to be_empty - expect(subject.send(:filter_clauses)).to be_empty - end - end - - context 'with \'"hello world"\'' do - let(:query) { '"hello world"' } - - it 'transforms clauses' do - expect(subject.send(:must_clauses).map(&:phrase)).to contain_exactly('hello world') - expect(subject.send(:must_not_clauses)).to be_empty - expect(subject.send(:filter_clauses)).to be_empty - end - end - - context 'with \'before:"2022-01-01 23:00"\'' do - let(:query) { 'before:"2022-01-01 23:00"' } - - it 'transforms clauses' do - expect(subject.send(:must_clauses)).to be_empty - expect(subject.send(:must_not_clauses)).to be_empty - expect(subject.send(:filter_clauses).map(&:term)).to contain_exactly(lt: '2022-01-01 23:00', time_zone: 'UTC') - end - end -end diff --git a/spec/lib/signature_parser_spec.rb b/spec/lib/signature_parser_spec.rb deleted file mode 100644 index 3f398e8dd0..0000000000 --- a/spec/lib/signature_parser_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe SignatureParser do - describe '.parse' do - subject { described_class.parse(header) } - - context 'with Signature headers conforming to draft-cavage-http-signatures-12' do - let(:header) do - # This example signature string deliberately mixes uneven spacing - # and quoting styles to ensure everything is covered - 'keyId = "https://remote.domain/users/bob#main-key,",algorithm= rsa-sha256 , headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="' # rubocop:disable Layout/LineLength - end - - it 'correctly parses the header' do - expect(subject).to eq({ - 'keyId' => 'https://remote.domain/users/bob#main-key,', - 'algorithm' => 'rsa-sha256', - 'headers' => 'host date digest (request-target)', - 'signature' => 'gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ==', # rubocop:disable Layout/LineLength - }) - end - end - - context 'with a malformed Signature header' do - let(:header) { 'hello this is malformed!' } - - it 'raises an error' do - expect { subject }.to raise_error(described_class::ParsingError) - end - end - end -end diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb deleted file mode 100644 index 5b80ccb970..0000000000 --- a/spec/lib/status_cache_hydrator_spec.rb +++ /dev/null @@ -1,147 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe StatusCacheHydrator do - let(:status) { Fabricate(:status) } - let(:account) { Fabricate(:account) } - - describe '#hydrate' do - let(:compare_to_hash) { InlineRenderer.render(status, account, :status) } - - shared_examples 'shared behavior' do - context 'when handling a new status' do - let(:poll) { Fabricate(:poll) } - let(:status) { Fabricate(:status, poll: poll) } - - it 'renders the same attributes as a full render' do - expect(subject).to eql(compare_to_hash) - end - end - - context 'when handling a new status with own poll' do - let(:poll) { Fabricate(:poll, account: account) } - let(:status) { Fabricate(:status, poll: poll, account: account) } - - it 'renders the same attributes as a full render' do - expect(subject).to eql(compare_to_hash) - end - end - - context 'when handling a filtered status' do - let(:status) { Fabricate(:status, text: 'this toot is about that banned word') } - - before do - account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }]) - end - - it 'renders the same attributes as a full render' do - expect(subject).to eql(compare_to_hash) - end - end - - context 'when handling a reblog' do - let(:reblog) { Fabricate(:status) } - let(:status) { Fabricate(:status, reblog: reblog) } - - context 'when it has been favourited' do - before do - FavouriteService.new.call(account, reblog) - end - - it 'renders the same attributes as a full render' do - expect(subject).to eql(compare_to_hash) - end - end - - context 'when it has been reblogged' do - before do - ReblogService.new.call(account, reblog) - end - - it 'renders the same attributes as a full render' do - expect(subject).to eql(compare_to_hash) - end - end - - context 'when it has been pinned' do - let(:reblog) { Fabricate(:status, account: account) } - - before do - StatusPin.create!(account: account, status: reblog) - end - - it 'renders the same attributes as a full render' do - expect(subject).to eql(compare_to_hash) - end - end - - context 'when it has been followed tags' do - let(:followed_tag) { Fabricate(:tag) } - - before do - reblog.tags << Fabricate(:tag) - reblog.tags << followed_tag - TagFollow.create!(tag: followed_tag, account: account, rate_limit: false) - end - - it 'renders the same attributes as a full render' do - expect(subject).to eql(compare_to_hash) - end - end - - context 'when it has a poll authored by the user' do - let(:poll) { Fabricate(:poll, account: account) } - let(:reblog) { Fabricate(:status, poll: poll, account: account) } - - it 'renders the same attributes as a full render' do - expect(subject).to eql(compare_to_hash) - end - end - - context 'when it has been voted in' do - let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) } - let(:reblog) { Fabricate(:status, poll: poll) } - - before do - VoteService.new.call(account, poll, [0]) - end - - it 'renders the same attributes as a full render' do - expect(subject).to eql(compare_to_hash) - end - end - - context 'when it matches account filters' do - let(:reblog) { Fabricate(:status, text: 'this toot is about that banned word') } - - before do - account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }]) - end - - it 'renders the same attributes as a full render' do - expect(subject).to eql(compare_to_hash) - end - end - end - end - - context 'when cache is warm' do - subject do - Rails.cache.write("fan-out/#{status.id}", InlineRenderer.render(status, nil, :status)) - described_class.new(status).hydrate(account.id) - end - - it_behaves_like 'shared behavior' - end - - context 'when cache is cold' do - subject do - Rails.cache.delete("fan-out/#{status.id}") - described_class.new(status).hydrate(account.id) - end - - it_behaves_like 'shared behavior' - end - end -end diff --git a/spec/lib/status_filter_spec.rb b/spec/lib/status_filter_spec.rb deleted file mode 100644 index cf6f3c7959..0000000000 --- a/spec/lib/status_filter_spec.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe StatusFilter do - describe '#filtered?' do - let(:status) { Fabricate(:status) } - - context 'without an account' do - subject(:filter) { described_class.new(status, nil) } - - context 'when there are no connections' do - it { is_expected.to_not be_filtered } - end - - context 'when status account is silenced' do - before do - status.account.silence! - end - - it { is_expected.to be_filtered } - end - - context 'when status policy does not allow show' do - it 'filters the status' do - policy = instance_double(StatusPolicy, show?: false) - allow(StatusPolicy).to receive(:new).and_return(policy) - - expect(filter).to be_filtered - end - end - end - - context 'with real account' do - subject(:filter) { described_class.new(status, account) } - - let(:account) { Fabricate(:account) } - - context 'when there are no connections' do - it { is_expected.to_not be_filtered } - end - - context 'when status account is blocked' do - before do - Fabricate(:block, account: account, target_account: status.account) - end - - it { is_expected.to be_filtered } - end - - context 'when status account domain is blocked' do - before do - status.account.update(domain: 'example.com') - Fabricate(:account_domain_block, account: account, domain: status.account_domain) - end - - it { is_expected.to be_filtered } - end - - context 'when status account is muted' do - before do - Fabricate(:mute, account: account, target_account: status.account) - end - - it { is_expected.to be_filtered } - end - - context 'when status account is silenced' do - before do - status.account.silence! - end - - it { is_expected.to be_filtered } - end - - context 'when status policy does not allow show' do - it 'filters the status' do - policy = instance_double(StatusPolicy, show?: false) - allow(StatusPolicy).to receive(:new).and_return(policy) - - expect(filter).to be_filtered - end - end - end - end -end diff --git a/spec/lib/status_finder_spec.rb b/spec/lib/status_finder_spec.rb deleted file mode 100644 index 53f5039af9..0000000000 --- a/spec/lib/status_finder_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe StatusFinder do - include RoutingHelper - - describe '#status' do - subject { described_class.new(url) } - - context 'with a status url' do - let(:status) { Fabricate(:status) } - let(:url) { short_account_status_url(account_username: status.account.username, id: status.id) } - - it 'finds the stream entry' do - expect(subject.status).to eq(status) - end - - it 'raises an error if action is not :show' do - recognized = Rails.application.routes.recognize_path(url) - allow(recognized).to receive(:[]).with(:action).and_return(:create) - allow(Rails.application.routes).to receive(:recognize_path).with(url).and_return(recognized) - - expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) - - expect(Rails.application.routes).to have_received(:recognize_path) - expect(recognized).to have_received(:[]) - end - end - - context 'with a remote url even if id exists on local' do - let(:status) { Fabricate(:status) } - let(:url) { "https://example.com/users/test/statuses/#{status.id}" } - - it 'raises an error' do - expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'with a plausible url' do - let(:url) { 'https://example.com/users/test/updates/123/embed' } - - it 'raises an error' do - expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'with an unrecognized url' do - let(:url) { 'https://example.com/about' } - - it 'raises an error' do - expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end -end diff --git a/spec/lib/status_reach_finder_spec.rb b/spec/lib/status_reach_finder_spec.rb deleted file mode 100644 index 7181717dc1..0000000000 --- a/spec/lib/status_reach_finder_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe StatusReachFinder do - describe '#inboxes' do - context 'with a local status' do - subject { described_class.new(status) } - - let(:parent_status) { nil } - let(:visibility) { :public } - let(:alice) { Fabricate(:account, username: 'alice') } - let(:status) { Fabricate(:status, account: alice, thread: parent_status, visibility: visibility) } - - context 'when it contains mentions of remote accounts' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') } - - before do - status.mentions.create!(account: bob) - end - - it 'includes the inbox of the mentioned account' do - expect(subject.inboxes).to include 'https://foo.bar/inbox' - end - end - - context 'when it has been reblogged by a remote account' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') } - - before do - bob.statuses.create!(reblog: status) - end - - it 'includes the inbox of the reblogger' do - expect(subject.inboxes).to include 'https://foo.bar/inbox' - end - - context 'when status is not public' do - let(:visibility) { :private } - - it 'does not include the inbox of the reblogger' do - expect(subject.inboxes).to_not include 'https://foo.bar/inbox' - end - end - end - - context 'when it has been favourited by a remote account' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') } - - before do - bob.favourites.create!(status: status) - end - - it 'includes the inbox of the favouriter' do - expect(subject.inboxes).to include 'https://foo.bar/inbox' - end - - context 'when status is not public' do - let(:visibility) { :private } - - it 'does not include the inbox of the favouriter' do - expect(subject.inboxes).to_not include 'https://foo.bar/inbox' - end - end - end - - context 'when it has been replied to by a remote account' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') } - - before do - bob.statuses.create!(thread: status, text: 'Hoge') - end - - it 'includes the inbox of the replier' do - expect(subject.inboxes).to include 'https://foo.bar/inbox' - end - - context 'when status is not public' do - let(:visibility) { :private } - - it 'does not include the inbox of the replier' do - expect(subject.inboxes).to_not include 'https://foo.bar/inbox' - end - end - end - - context 'when it is a reply to a remote account' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') } - let(:parent_status) { Fabricate(:status, account: bob) } - - it 'includes the inbox of the replied-to account' do - expect(subject.inboxes).to include 'https://foo.bar/inbox' - end - - context 'when status is not public and replied-to account is not mentioned' do - let(:visibility) { :private } - - it 'does not include the inbox of the replied-to account' do - expect(subject.inboxes).to_not include 'https://foo.bar/inbox' - end - end - end - end - end -end diff --git a/spec/lib/suspicious_sign_in_detector_spec.rb b/spec/lib/suspicious_sign_in_detector_spec.rb deleted file mode 100644 index 9e64aff08a..0000000000 --- a/spec/lib/suspicious_sign_in_detector_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe SuspiciousSignInDetector do - describe '#suspicious?' do - subject { described_class.new(user).suspicious?(request) } - - let(:user) { Fabricate(:user, current_sign_in_at: 1.day.ago) } - let(:request) { instance_double(ActionDispatch::Request, remote_ip: remote_ip) } - let(:remote_ip) { nil } - - context 'when user has 2FA enabled' do - before do - user.update!(otp_required_for_login: true) - end - - it 'returns false' do - expect(subject).to be false - end - end - - context 'when exact IP has been used before' do - let(:remote_ip) { '1.1.1.1' } - - before do - user.update!(sign_up_ip: remote_ip) - end - - it 'returns false' do - expect(subject).to be false - end - end - - context 'when similar IP has been used before' do - let(:remote_ip) { '1.1.2.2' } - - before do - user.update!(sign_up_ip: '1.1.1.1') - end - - it 'returns false' do - expect(subject).to be false - end - end - - context 'when IP is completely unfamiliar' do - let(:remote_ip) { '2.2.2.2' } - - before do - user.update!(sign_up_ip: '1.1.1.1') - end - - it 'returns true' do - expect(subject).to be true - end - end - end -end diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb deleted file mode 100644 index 38203a55f7..0000000000 --- a/spec/lib/tag_manager_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe TagManager do - describe '#local_domain?' do - # The following comparisons MUST be case-insensitive. - - around do |example| - original_local_domain = Rails.configuration.x.local_domain - Rails.configuration.x.local_domain = 'domain.example.com' - - example.run - - Rails.configuration.x.local_domain = original_local_domain - end - - it 'returns true for nil' do - expect(described_class.instance.local_domain?(nil)).to be true - end - - it 'returns true if the slash-stripped string equals to local domain' do - expect(described_class.instance.local_domain?('DoMaIn.Example.com/')).to be true - end - - it 'returns false for irrelevant string' do - expect(described_class.instance.local_domain?('DoMaIn.Example.com!')).to be false - end - end - - describe '#web_domain?' do - # The following comparisons MUST be case-insensitive. - - around do |example| - original_web_domain = Rails.configuration.x.web_domain - Rails.configuration.x.web_domain = 'domain.example.com' - - example.run - - Rails.configuration.x.web_domain = original_web_domain - end - - it 'returns true for nil' do - expect(described_class.instance.web_domain?(nil)).to be true - end - - it 'returns true if the slash-stripped string equals to web domain' do - expect(described_class.instance.web_domain?('DoMaIn.Example.com/')).to be true - end - - it 'returns false for string with irrelevant characters' do - expect(described_class.instance.web_domain?('DoMaIn.Example.com!')).to be false - end - end - - describe '#normalize_domain' do - it 'returns nil if the given parameter is nil' do - expect(described_class.instance.normalize_domain(nil)).to be_nil - end - - it 'returns normalized domain' do - expect(described_class.instance.normalize_domain('DoMaIn.Example.com/')).to eq 'domain.example.com' - end - end - - describe '#local_url?' do - around do |example| - original_web_domain = Rails.configuration.x.web_domain - example.run - Rails.configuration.x.web_domain = original_web_domain - end - - it 'returns true if the normalized string with port is local URL' do - Rails.configuration.x.web_domain = 'domain.example.com:42' - expect(described_class.instance.local_url?('https://DoMaIn.Example.com:42/')).to be true - end - - it 'returns true if the normalized string without port is local URL' do - Rails.configuration.x.web_domain = 'domain.example.com' - expect(described_class.instance.local_url?('https://DoMaIn.Example.com/')).to be true - end - - it 'returns false for string with irrelevant characters' do - Rails.configuration.x.web_domain = 'domain.example.com' - expect(described_class.instance.local_url?('https://domain.example.net/')).to be false - end - end -end diff --git a/spec/lib/text_formatter_spec.rb b/spec/lib/text_formatter_spec.rb deleted file mode 100644 index bde17bb79c..0000000000 --- a/spec/lib/text_formatter_spec.rb +++ /dev/null @@ -1,323 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe TextFormatter do - describe '#to_s' do - subject { described_class.new(text, preloaded_accounts: preloaded_accounts).to_s } - - let(:preloaded_accounts) { nil } - - context 'when given text containing plain text' do - let(:text) { 'text' } - - it 'paragraphizes the text' do - expect(subject).to eq '

text

' - end - end - - context 'when given text containing line feeds' do - let(:text) { "line\nfeed" } - - it 'removes line feeds' do - expect(subject).to_not include "\n" - end - end - - context 'when given text containing linkable mentions' do - let(:preloaded_accounts) { [Fabricate(:account, username: 'alice')] } - let(:text) { '@alice' } - - it 'creates a mention link' do - expect(subject).to include '@alice' - end - end - - context 'when given text containing unlinkable mentions' do - let(:preloaded_accounts) { [] } - let(:text) { '@alice' } - - it 'does not create a mention link' do - expect(subject).to include '@alice' - end - end - - context 'when given a stand-alone medium URL' do - let(:text) { 'https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4' } - - it 'matches the full URL' do - expect(subject).to include 'href="https://hackernoon.com/the-power-to-build-communities-a-response-to-mark-zuckerberg-3f2cac9148a4"' - end - end - - context 'when given a stand-alone google URL' do - let(:text) { 'http://google.com' } - - it 'matches the full URL' do - expect(subject).to include 'href="http://google.com"' - end - end - - context 'when given a stand-alone URL with a newer TLD' do - let(:text) { 'http://example.gay' } - - it 'matches the full URL' do - expect(subject).to include 'href="http://example.gay"' - end - end - - context 'when given a stand-alone IDN URL' do - let(:text) { 'https://nic.みんな/' } - - it 'matches the full URL' do - expect(subject).to include 'href="https://nic.みんな/"' - end - - it 'has display URL' do - expect(subject).to include 'nic.みんな/' - end - end - - context 'when given a URL with a trailing period' do - let(:text) { 'http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona. ' } - - it 'matches the full URL but not the period' do - expect(subject).to include 'href="http://www.mcmansionhell.com/post/156408871451/50-states-of-mcmansion-hell-scottsdale-arizona"' - end - end - - context 'when given a URL enclosed with parentheses' do - let(:text) { '(http://google.com/)' } - - it 'matches the full URL but not the parentheses' do - expect(subject).to include 'href="http://google.com/"' - end - end - - context 'when given a URL with a trailing exclamation point' do - let(:text) { 'http://www.google.com!' } - - it 'matches the full URL but not the exclamation point' do - expect(subject).to include 'href="http://www.google.com"' - end - end - - context 'when given a URL with a trailing single quote' do - let(:text) { "http://www.google.com'" } - - it 'matches the full URL but not the single quote' do - expect(subject).to include 'href="http://www.google.com"' - end - end - - context 'when given a URL with a trailing angle bracket' do - let(:text) { 'http://www.google.com>' } - - it 'matches the full URL but not the angle bracket' do - expect(subject).to include 'href="http://www.google.com"' - end - end - - context 'when given a URL with a query string' do - context 'with escaped unicode character' do - let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' } - - it 'matches the full URL' do - expect(subject).to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink"' - end - end - - context 'with unicode character' do - let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓&q=autolink' } - - it 'matches the full URL' do - expect(subject).to include 'href="https://www.ruby-toolbox.com/search?utf8=✓&q=autolink"' - end - end - - context 'with unicode character at the end' do - let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓' } - - it 'matches the full URL' do - expect(subject).to include 'href="https://www.ruby-toolbox.com/search?utf8=✓"' - end - end - - context 'with escaped and not escaped unicode characters' do - let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink' } - - it 'preserves escaped unicode characters' do - expect(subject).to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink"' - end - end - end - - context 'when given a URL with parentheses in it' do - let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' } - - it 'matches the full URL' do - expect(subject).to include 'href="https://en.wikipedia.org/wiki/Diaspora_(software)"' - end - end - - context 'when given a URL in quotation marks' do - let(:text) { '"https://example.com/"' } - - it 'does not match the quotation marks' do - expect(subject).to include 'href="https://example.com/"' - end - end - - context 'when given a URL in angle brackets' do - let(:text) { '' } - - it 'does not match the angle brackets' do - expect(subject).to include 'href="https://example.com/"' - end - end - - context 'when given a URL with Japanese path string' do - let(:text) { 'https://ja.wikipedia.org/wiki/日本' } - - it 'matches the full URL' do - expect(subject).to include 'href="https://ja.wikipedia.org/wiki/日本"' - end - end - - context 'when given a URL with Korean path string' do - let(:text) { 'https://ko.wikipedia.org/wiki/대한민국' } - - it 'matches the full URL' do - expect(subject).to include 'href="https://ko.wikipedia.org/wiki/대한민국"' - end - end - - context 'when given a URL with a full-width space' do - let(:text) { 'https://example.com/ abc123' } - - it 'does not match the full-width space' do - expect(subject).to include 'href="https://example.com/"' - end - end - - context 'when given a URL in Japanese quotation marks' do - let(:text) { '「[https://example.org/」' } - - it 'does not match the quotation marks' do - expect(subject).to include 'href="https://example.org/"' - end - end - - context 'when given a URL with Simplified Chinese path string' do - let(:text) { 'https://baike.baidu.com/item/中华人民共和国' } - - it 'matches the full URL' do - expect(subject).to include 'href="https://baike.baidu.com/item/中华人民共和国"' - end - end - - context 'when given a URL with Traditional Chinese path string' do - let(:text) { 'https://zh.wikipedia.org/wiki/臺灣' } - - it 'matches the full URL' do - expect(subject).to include 'href="https://zh.wikipedia.org/wiki/臺灣"' - end - end - - context 'when given a URL with trailing @ symbol' do - let(:text) { 'https://gta.fandom.com/wiki/TW@ Content' } - - it 'matches the full URL' do - expect(subject).to include 'href="https://gta.fandom.com/wiki/TW@"' - end - end - - context 'when given a URL containing unsafe code (XSS attack, visible part)' do - let(:text) { 'http://example.com/bb' } - - it 'does not include the HTML in the URL' do - expect(subject).to include '"http://example.com/b"' - end - - it 'escapes the HTML' do - expect(subject).to include '<del>b</del>' - end - end - - context 'when given a URL containing unsafe code (XSS attack, invisible part)' do - let(:text) { 'http://example.com/blahblahblahblah/a' } - - it 'does not include the HTML in the URL' do - expect(subject).to include '"http://example.com/blahblahblahblah/a"' - end - - it 'escapes the HTML' do - expect(subject).to include '<script>alert("Hello")</script>' - end - end - - context 'when given text containing HTML code (script tag)' do - let(:text) { '' } - - it 'escapes the HTML' do - expect(subject).to include '

<script>alert("Hello")</script>

' - end - end - - context 'when given text containing HTML (XSS attack)' do - let(:text) { %q{} } - - it 'escapes the HTML' do - expect(subject).to include '

<img src="javascript:alert('XSS');">

' - end - end - - context 'when given an invalid URL' do - let(:text) { 'http://www\.google\.com' } - - it 'outputs the raw URL' do - expect(subject).to eq '

http://www\.google\.com

' - end - end - - context 'when given text containing a hashtag' do - let(:text) { '#hashtag' } - - it 'creates a hashtag link' do - expect(subject).to include '/tags/hashtag" class="mention hashtag" rel="tag">#hashtag' - end - end - - context 'when given text containing a hashtag with Unicode chars' do - let(:text) { '#hashtagタグ' } - - it 'creates a hashtag link' do - expect(subject).to include '/tags/hashtag%E3%82%BF%E3%82%B0" class="mention hashtag" rel="tag">#hashtagタグ' - end - end - - context 'when given text with a stand-alone xmpp: URI' do - let(:text) { 'xmpp:user@instance.com' } - - it 'matches the full URI' do - expect(subject).to include 'href="xmpp:user@instance.com"' - end - end - - context 'when given text with an xmpp: URI with a query-string' do - let(:text) { 'please join xmpp:muc@instance.com?join right now' } - - it 'matches the full URI' do - expect(subject).to include 'href="xmpp:muc@instance.com?join"' - end - end - - context 'when given text containing a magnet: URI' do - let(:text) { 'wikipedia gives this example of a magnet uri: magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a' } - - it 'matches the full URI' do - expect(subject).to include 'href="magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a"' - end - end - end -end diff --git a/spec/lib/translation_service/deepl_spec.rb b/spec/lib/translation_service/deepl_spec.rb deleted file mode 100644 index 4797a3dc63..0000000000 --- a/spec/lib/translation_service/deepl_spec.rb +++ /dev/null @@ -1,99 +0,0 @@ -# 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 - %w(api-free.deepl.com api.deepl.com).each do |host| - stub_request(:get, "https://#{host}/v2/languages?type=source").to_return( - body: '[{"language":"EN","name":"English"},{"language":"UK","name":"Ukrainian"}]' - ) - stub_request(:get, "https://#{host}/v2/languages?type=target").to_return( - body: '[{"language":"EN-GB","name":"English (British)"},{"language":"ZH","name":"Chinese"}]' - ) - 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"}]}') - - translations = service.translate(['Hasta la vista'], 'es', 'en') - expect(translations.size).to eq 1 - - translation = translations.first - 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"}]}') - - translations = service.translate(['Guten Tag'], nil, 'en') - expect(translations.size).to eq 1 - - translation = translations.first - expect(translation.detected_source_language).to eq 'de' - expect(translation.provider).to eq 'DeepL.com' - expect(translation.text).to eq 'Good morning' - end - - it 'returns translation of multiple texts' do - stub_request(:post, 'https://api.deepl.com/v2/translate') - .with(body: 'text=Guten+Morgen&text=Gute+Nacht&source_lang=DE&target_lang=en&tag_handling=html') - .to_return(body: '{"translations":[{"detected_source_language":"DE","text":"Good morning"},{"detected_source_language":"DE","text":"Good night"}]}') - - translations = service.translate(['Guten Morgen', 'Gute Nacht'], 'de', 'en') - expect(translations.size).to eq 2 - - expect(translations.first.text).to eq 'Good morning' - expect(translations.last.text).to eq 'Good night' - end - end - - describe '#languages' do - it 'returns source languages' do - expect(service.languages.keys).to eq [nil, 'en', 'uk'] - end - - it 'returns target languages for each source language' do - expect(service.languages['en']).to eq %w(pt en-GB zh) - expect(service.languages['uk']).to eq %w(en pt en-GB zh) - end - - it 'returns target languages for auto-detection' do - expect(service.languages[nil]).to eq %w(en pt en-GB zh) - end - end - - describe 'the paid and free plan api hostnames' do - before do - service.languages - end - - context 'without a plan set' do - it 'uses paid plan base URL and sends an API key' do - expect(a_request(:get, 'https://api.deepl.com/v2/languages?type=source').with(headers: { Authorization: 'DeepL-Auth-Key my-api-key' })).to have_been_made.once - expect(a_request(:get, 'https://api.deepl.com/v2/languages?type=target').with(headers: { Authorization: 'DeepL-Auth-Key my-api-key' })).to have_been_made.once - end - end - - context 'with the free plan' do - let(:plan) { 'free' } - - it 'uses free plan base URL and sends an API key' do - expect(a_request(:get, 'https://api-free.deepl.com/v2/languages?type=source').with(headers: { Authorization: 'DeepL-Auth-Key my-api-key' })).to have_been_made.once - expect(a_request(:get, 'https://api-free.deepl.com/v2/languages?type=target').with(headers: { Authorization: 'DeepL-Auth-Key my-api-key' })).to have_been_made.once - end - end - end -end diff --git a/spec/lib/translation_service/libre_translate_spec.rb b/spec/lib/translation_service/libre_translate_spec.rb deleted file mode 100644 index 90966a8ebf..0000000000 --- a/spec/lib/translation_service/libre_translate_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# 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","en","es"]},{"code": "da","name": "Danish","targets": ["en","pt"]}]' - ) - end - - describe '#languages' do - subject(:languages) { service.languages } - - it 'returns source languages' do - expect(languages.keys).to eq ['en', 'da', nil] - end - - it 'returns target languages for each source language' do - expect(languages['en']).to eq %w(de es) - expect(languages['da']).to eq %w(en pt) - end - - it 'returns target languages for auto-detected language' do - expect(languages[nil]).to eq %w(de en es pt) - 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"]}') - - translations = service.translate(['Hasta la vista'], 'es', 'en') - expect(translations.size).to eq 1 - - translation = translations.first - expect(translation.detected_source_language).to be '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"]}') - - translations = service.translate(['Guten Morgen'], nil, 'en') - expect(translations.size).to eq 1 - - translation = translations.first - expect(translation.detected_source_language).to eq 'de' - expect(translation.provider).to eq 'LibreTranslate' - expect(translation.text).to eq 'Good morning' - end - - it 'returns translation of multiple texts' do - stub_request(:post, 'https://libretranslate.example.com/translate') - .with(body: '{"q":["Guten Morgen","Gute Nacht"],"source":"de","target":"en","format":"html","api_key":"my-api-key"}') - .to_return(body: '{"translatedText": ["Good morning", "Good night"]}') - - translations = service.translate(['Guten Morgen', 'Gute Nacht'], 'de', 'en') - expect(translations.size).to eq 2 - - expect(translations.first.text).to eq 'Good morning' - expect(translations.last.text).to eq 'Good night' - end - end -end diff --git a/spec/lib/vacuum/access_tokens_vacuum_spec.rb b/spec/lib/vacuum/access_tokens_vacuum_spec.rb deleted file mode 100644 index 54760c41bd..0000000000 --- a/spec/lib/vacuum/access_tokens_vacuum_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Vacuum::AccessTokensVacuum do - subject { described_class.new } - - describe '#perform' do - let!(:revoked_access_token) { Fabricate(:access_token, revoked_at: 1.minute.ago) } - let!(:expired_access_token) { Fabricate(:access_token, expires_in: 59.minutes.to_i, created_at: 1.hour.ago) } - let!(:active_access_token) { Fabricate(:access_token) } - - let!(:revoked_access_grant) { Fabricate(:access_grant, revoked_at: 1.minute.ago) } - let!(:expired_access_grant) { Fabricate(:access_grant, expires_in: 59.minutes.to_i, created_at: 1.hour.ago) } - let!(:active_access_grant) { Fabricate(:access_grant) } - - before do - subject.perform - end - - it 'deletes revoked access tokens' do - expect { revoked_access_token.reload }.to raise_error ActiveRecord::RecordNotFound - end - - it 'deletes expired access tokens' do - expect { expired_access_token.reload }.to raise_error ActiveRecord::RecordNotFound - end - - it 'deletes revoked access grants' do - expect { revoked_access_grant.reload }.to raise_error ActiveRecord::RecordNotFound - end - - it 'deletes expired access grants' do - expect { expired_access_grant.reload }.to raise_error ActiveRecord::RecordNotFound - end - - it 'does not delete active access tokens' do - expect { active_access_token.reload }.to_not raise_error - end - - it 'does not delete active access grants' do - expect { active_access_grant.reload }.to_not raise_error - end - end -end diff --git a/spec/lib/vacuum/backups_vacuum_spec.rb b/spec/lib/vacuum/backups_vacuum_spec.rb deleted file mode 100644 index 867dbe4020..0000000000 --- a/spec/lib/vacuum/backups_vacuum_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Vacuum::BackupsVacuum do - subject { described_class.new(retention_period) } - - let(:retention_period) { 7.days } - - describe '#perform' do - let!(:expired_backup) { Fabricate(:backup, created_at: (retention_period + 1.day).ago) } - let!(:current_backup) { Fabricate(:backup) } - - before do - subject.perform - end - - it 'deletes backups past the retention period' do - expect { expired_backup.reload }.to raise_error ActiveRecord::RecordNotFound - end - - it 'does not delete backups within the retention period' do - expect { current_backup.reload }.to_not raise_error - end - end -end diff --git a/spec/lib/vacuum/feeds_vacuum_spec.rb b/spec/lib/vacuum/feeds_vacuum_spec.rb deleted file mode 100644 index ede1e3c360..0000000000 --- a/spec/lib/vacuum/feeds_vacuum_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Vacuum::FeedsVacuum do - subject { described_class.new } - - describe '#perform' do - let!(:active_user) { Fabricate(:user, current_sign_in_at: 2.days.ago) } - let!(:inactive_user) { Fabricate(:user, current_sign_in_at: 22.days.ago) } - - before do - redis.zadd(feed_key_for(inactive_user), 1, 1) - redis.zadd(feed_key_for(active_user), 1, 1) - redis.zadd(feed_key_for(inactive_user, 'reblogs'), 2, 2) - redis.sadd(feed_key_for(inactive_user, 'reblogs:2'), 3) - - subject.perform - end - - it 'clears feeds of inactive users and lists' do - expect(redis.zcard(feed_key_for(inactive_user))).to eq 0 - expect(redis.zcard(feed_key_for(active_user))).to eq 1 - expect(redis.exists?(feed_key_for(inactive_user, 'reblogs'))).to be false - expect(redis.exists?(feed_key_for(inactive_user, 'reblogs:2'))).to be false - end - end - - def feed_key_for(user, subtype = nil) - FeedManager.instance.key(:home, user.account_id, subtype) - end -end diff --git a/spec/lib/vacuum/imports_vacuum_spec.rb b/spec/lib/vacuum/imports_vacuum_spec.rb deleted file mode 100644 index 3a273d8276..0000000000 --- a/spec/lib/vacuum/imports_vacuum_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Vacuum::ImportsVacuum do - subject { described_class.new } - - let!(:old_unconfirmed) { Fabricate(:bulk_import, state: :unconfirmed, created_at: 2.days.ago) } - let!(:new_unconfirmed) { Fabricate(:bulk_import, state: :unconfirmed, created_at: 10.seconds.ago) } - let!(:recent_ongoing) { Fabricate(:bulk_import, state: :in_progress, created_at: 20.minutes.ago) } - let!(:recent_finished) { Fabricate(:bulk_import, state: :finished, created_at: 1.day.ago) } - let!(:old_finished) { Fabricate(:bulk_import, state: :finished, created_at: 2.months.ago) } - - describe '#perform' do - it 'cleans up the expected imports' do - expect { subject.perform } - .to change { ordered_bulk_imports.pluck(:id) } - .from(original_import_ids) - .to(remaining_import_ids) - end - - def ordered_bulk_imports - BulkImport.order(id: :asc) - end - - def original_import_ids - [old_unconfirmed, new_unconfirmed, recent_ongoing, recent_finished, old_finished].map(&:id) - end - - def vacuumed_import_ids - [old_unconfirmed, old_finished].map(&:id) - end - - def remaining_import_ids - original_import_ids - vacuumed_import_ids - end - end -end diff --git a/spec/lib/vacuum/media_attachments_vacuum_spec.rb b/spec/lib/vacuum/media_attachments_vacuum_spec.rb deleted file mode 100644 index 1039c36cea..0000000000 --- a/spec/lib/vacuum/media_attachments_vacuum_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Vacuum::MediaAttachmentsVacuum do - subject { described_class.new(retention_period) } - - let(:retention_period) { 7.days } - let(:remote_status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com')) } - let(:local_status) { Fabricate(:status) } - - describe '#perform' do - let!(:old_remote_media) { Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png', status: remote_status, created_at: (retention_period + 1.day).ago, updated_at: (retention_period + 1.day).ago) } - let!(:old_local_media) { Fabricate(:media_attachment, status: local_status, created_at: (retention_period + 1.day).ago, updated_at: (retention_period + 1.day).ago) } - let!(:new_remote_media) { Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png', status: remote_status) } - let!(:new_local_media) { Fabricate(:media_attachment, status: local_status) } - let!(:old_unattached_media) { Fabricate(:media_attachment, account_id: nil, created_at: 10.days.ago) } - let!(:new_unattached_media) { Fabricate(:media_attachment, account_id: nil, created_at: 1.hour.ago) } - - before { subject.perform } - - it 'handles attachments based on metadata details' do - expect(old_remote_media.reload.file) # Remote and past retention period - .to be_blank - expect(old_local_media.reload.file) # Local and past retention - .to_not be_blank - expect(new_remote_media.reload.file) # Remote and within retention - .to_not be_blank - expect(new_local_media.reload.file) # Local and within retention - .to_not be_blank - expect { old_unattached_media.reload } # Unattached and past TTL - .to raise_error(ActiveRecord::RecordNotFound) - expect(new_unattached_media.reload) # Unattached and within TTL - .to be_persisted - end - end -end diff --git a/spec/lib/vacuum/preview_cards_vacuum_spec.rb b/spec/lib/vacuum/preview_cards_vacuum_spec.rb deleted file mode 100644 index 9dbdf0bc2f..0000000000 --- a/spec/lib/vacuum/preview_cards_vacuum_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Vacuum::PreviewCardsVacuum do - subject { described_class.new(retention_period) } - - let(:retention_period) { 7.days } - - describe '#perform' do - let!(:orphaned_preview_card) { Fabricate(:preview_card, created_at: 2.days.ago) } - let!(:old_preview_card) { Fabricate(:preview_card, updated_at: (retention_period + 1.day).ago) } - let!(:new_preview_card) { Fabricate(:preview_card) } - - before do - old_preview_card.statuses << Fabricate(:status) - new_preview_card.statuses << Fabricate(:status) - - subject.perform - end - - it 'deletes cache of preview cards last updated before the retention period' do - expect(old_preview_card.reload.image).to be_blank - end - - it 'does not delete cache of preview cards last updated within the retention period' do - expect(new_preview_card.reload.image).to_not be_blank - end - - it 'does not delete attached preview cards' do - expect(new_preview_card.reload).to be_persisted - end - - it 'does not delete orphaned preview cards in the retention period' do - expect(orphaned_preview_card.reload).to be_persisted - end - end -end diff --git a/spec/lib/vacuum/statuses_vacuum_spec.rb b/spec/lib/vacuum/statuses_vacuum_spec.rb deleted file mode 100644 index d5c0139506..0000000000 --- a/spec/lib/vacuum/statuses_vacuum_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Vacuum::StatusesVacuum do - subject { described_class.new(retention_period) } - - let(:retention_period) { 7.days } - - let(:remote_account) { Fabricate(:account, domain: 'example.com') } - - describe '#perform' do - let!(:remote_status_old) { Fabricate(:status, account: remote_account, created_at: (retention_period + 2.days).ago) } - let!(:remote_status_recent) { Fabricate(:status, account: remote_account, created_at: (retention_period - 2.days).ago) } - let!(:local_status_old) { Fabricate(:status, created_at: (retention_period + 2.days).ago) } - let!(:local_status_recent) { Fabricate(:status, created_at: (retention_period - 2.days).ago) } - - before do - subject.perform - end - - it 'deletes remote statuses past the retention period' do - expect { remote_status_old.reload }.to raise_error ActiveRecord::RecordNotFound - end - - it 'does not delete local statuses past the retention period' do - expect { local_status_old.reload }.to_not raise_error - end - - it 'does not delete remote statuses within the retention period' do - expect { remote_status_recent.reload }.to_not raise_error - end - - it 'does not delete local statuses within the retention period' do - expect { local_status_recent.reload }.to_not raise_error - end - end -end diff --git a/spec/lib/vacuum/system_keys_vacuum_spec.rb b/spec/lib/vacuum/system_keys_vacuum_spec.rb deleted file mode 100644 index 84cae30411..0000000000 --- a/spec/lib/vacuum/system_keys_vacuum_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Vacuum::SystemKeysVacuum do - subject { described_class.new } - - describe '#perform' do - let!(:expired_system_key) { Fabricate(:system_key, created_at: (SystemKey::ROTATION_PERIOD * 4).ago) } - let!(:current_system_key) { Fabricate(:system_key) } - - before do - subject.perform - end - - it 'deletes the expired key' do - expect { expired_system_key.reload }.to raise_error ActiveRecord::RecordNotFound - end - - it 'does not delete the current key' do - expect { current_system_key.reload }.to_not raise_error - end - end -end diff --git a/spec/lib/webfinger_resource_spec.rb b/spec/lib/webfinger_resource_spec.rb deleted file mode 100644 index 442f91aad0..0000000000 --- a/spec/lib/webfinger_resource_spec.rb +++ /dev/null @@ -1,144 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe WebfingerResource do - around do |example| - before_local = Rails.configuration.x.local_domain - before_web = Rails.configuration.x.web_domain - example.run - Rails.configuration.x.local_domain = before_local - Rails.configuration.x.web_domain = before_web - end - - describe '#username' do - describe 'with a URL value' do - it 'raises with a route whose controller is not AccountsController' do - resource = 'https://example.com/users/alice/other' - - expect do - described_class.new(resource).username - end.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'raises with a route whose action is not show' do - resource = 'https://example.com/users/alice' - - recognized = Rails.application.routes.recognize_path(resource) - allow(recognized).to receive(:[]).with(:controller).and_return('accounts') - allow(recognized).to receive(:[]).with(:username).and_return('alice') - allow(recognized).to receive(:[]).with(:action).and_return('create') - - allow(Rails.application.routes).to receive(:recognize_path).with(resource).and_return(recognized) - - expect do - described_class.new(resource).username - end.to raise_error(ActiveRecord::RecordNotFound) - expect(recognized).to have_received(:[]).exactly(3).times - - expect(Rails.application.routes).to have_received(:recognize_path) - .with(resource) - .at_least(:once) - end - - it 'raises with a string that doesnt start with URL' do - resource = 'website for http://example.com/users/alice/other' - - expect do - described_class.new(resource).username - end.to raise_error(described_class::InvalidRequest) - end - - it 'finds the username in a valid https route' do - resource = 'https://example.com/users/alice' - - result = described_class.new(resource).username - expect(result).to eq 'alice' - end - - it 'finds the username in a mixed case http route' do - resource = 'HTTp://exAMPLe.com/users/alice' - - result = described_class.new(resource).username - expect(result).to eq 'alice' - end - - it 'finds the username in a valid http route' do - resource = 'http://example.com/users/alice' - - result = described_class.new(resource).username - expect(result).to eq 'alice' - end - end - - describe 'with a username and hostname value' do - it 'raises on a non-local domain' do - resource = 'user@remote-host.com' - - expect do - described_class.new(resource).username - end.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'finds username for a local domain' do - Rails.configuration.x.local_domain = 'example.com' - resource = 'alice@example.com' - - result = described_class.new(resource).username - expect(result).to eq 'alice' - end - - it 'finds username for a web domain' do - Rails.configuration.x.web_domain = 'example.com' - resource = 'alice@example.com' - - result = described_class.new(resource).username - expect(result).to eq 'alice' - end - end - - describe 'with an acct value' do - it 'raises on a non-local domain' do - resource = 'acct:user@remote-host.com' - - expect do - described_class.new(resource).username - end.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'raises on a nonsense domain' do - resource = 'acct:user@remote-host@remote-hostess.remote.local@remote' - - expect do - described_class.new(resource).username - end.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'finds the username for a local account if the domain is the local one' do - Rails.configuration.x.local_domain = 'example.com' - resource = 'acct:alice@example.com' - - result = described_class.new(resource).username - expect(result).to eq 'alice' - end - - it 'finds the username for a local account if the domain is the Web one' do - Rails.configuration.x.web_domain = 'example.com' - resource = 'acct:alice@example.com' - - result = described_class.new(resource).username - expect(result).to eq 'alice' - end - end - - describe 'with a nonsense resource' do - it 'raises InvalidRequest' do - resource = 'df/:dfkj' - - expect do - described_class.new(resource).username - end.to raise_error(described_class::InvalidRequest) - end - end - end -end diff --git a/spec/lib/webfinger_spec.rb b/spec/lib/webfinger_spec.rb deleted file mode 100644 index 5015deac7f..0000000000 --- a/spec/lib/webfinger_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Webfinger do - describe 'self link' do - context 'when self link is specified with type application/activity+json' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } - - it 'correctly parses the response' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - - response = described_class.new('acct:alice@example.com').perform - - expect(response.self_link_href).to eq 'https://example.com/alice' - end - end - - context 'when self link is specified with type application/ld+json' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }] } } - - it 'correctly parses the response' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - - response = described_class.new('acct:alice@example.com').perform - - expect(response.self_link_href).to eq 'https://example.com/alice' - end - end - - context 'when self link is specified with incorrect type' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/json"' }] } } - - it 'raises an error' do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - - expect { described_class.new('acct:alice@example.com').perform }.to raise_error(Webfinger::Error) - end - end - end -end diff --git a/spec/lib/webhooks/payload_renderer_spec.rb b/spec/lib/webhooks/payload_renderer_spec.rb deleted file mode 100644 index 074847c74c..0000000000 --- a/spec/lib/webhooks/payload_renderer_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Webhooks::PayloadRenderer do - subject(:renderer) { described_class.new(json) } - - let(:event) { Webhooks::EventPresenter.new(type, object) } - let(:payload) { ActiveModelSerializers::SerializableResource.new(event, serializer: REST::Admin::WebhookEventSerializer, scope: nil, scope_name: :current_user).as_json } - let(:json) { Oj.dump(payload) } - - describe '#render' do - context 'when event is account.approved' do - let(:type) { 'account.approved' } - let(:object) { Fabricate(:account, display_name: 'Foo"') } - - it 'renders event-related variables into template' do - expect(renderer.render('foo={{event}}')).to eq 'foo=account.approved' - end - - it 'renders event-specific variables into template' do - expect(renderer.render('foo={{object.username}}')).to eq "foo=#{object.username}" - end - - it 'escapes values for use in JSON' do - expect(renderer.render('foo={{object.account.display_name}}')).to eq 'foo=Foo\\"' - end - end - end -end diff --git a/spec/locales/i18n_spec.rb b/spec/locales/i18n_spec.rb deleted file mode 100644 index cfce8e2234..0000000000 --- a/spec/locales/i18n_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'I18n' do - describe 'Pluralizing locale translations' do - subject { I18n.t('generic.validation_errors', count: 1) } - - context 'with the `en` locale which has `one` and `other` plural values' do - around do |example| - I18n.with_locale(:en) do - example.run - end - end - - it 'translates to `en` correctly and without error' do - expect { subject }.to_not raise_error - expect(subject).to match(/the error below/) - end - end - - context 'with the `my` locale which has only `other` plural value' do - around do |example| - I18n.with_locale(:my) do - example.run - end - end - - it 'translates to `my` correctly and without error' do - expect { subject }.to_not raise_error - expect(subject).to match(/1/) - end - end - end -end diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb deleted file mode 100644 index cd1ab3311c..0000000000 --- a/spec/mailers/admin_mailer_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AdminMailer do - describe '.new_report' do - let(:sender) { Fabricate(:account, username: 'John') } - let(:recipient) { Fabricate(:account, username: 'Mike') } - let(:report) { Fabricate(:report, account: sender, target_account: recipient) } - let(:mail) { described_class.with(recipient: recipient).new_report(report) } - - before do - recipient.user.update(locale: :en) - end - - it 'renders the email' do - expect(mail) - .to be_present - .and(deliver_to(recipient.user_email)) - .and(deliver_from('notifications@localhost')) - .and(have_subject("New report for cb6e6126.ngrok.io (##{report.id})")) - .and(have_body_text("Mike,\r\n\r\nJohn has reported Mike\r\n\r\nView: https://cb6e6126.ngrok.io/admin/reports/#{report.id}\r\n")) - end - end - - describe '.new_appeal' do - let(:appeal) { Fabricate(:appeal) } - let(:recipient) { Fabricate(:account, username: 'Kurt') } - let(:mail) { described_class.with(recipient: recipient).new_appeal(appeal) } - - before do - recipient.user.update(locale: :en) - end - - it 'renders the email' do - expect(mail) - .to be_present - .and(deliver_to(recipient.user_email)) - .and(deliver_from('notifications@localhost')) - .and(have_subject("#{appeal.account.username} is appealing a moderation decision on cb6e6126.ngrok.io")) - .and(have_body_text("#{appeal.account.username} is appealing a moderation decision by #{appeal.strike.account.username}")) - end - end - - describe '.new_pending_account' do - let(:recipient) { Fabricate(:account, username: 'Barklums') } - let(:user) { Fabricate(:user) } - let(:mail) { described_class.with(recipient: recipient).new_pending_account(user) } - - before do - recipient.user.update(locale: :en) - end - - it 'renders the email' do - expect(mail) - .to be_present - .and(deliver_to(recipient.user_email)) - .and(deliver_from('notifications@localhost')) - .and(have_subject("New account up for review on cb6e6126.ngrok.io (#{user.account.username})")) - .and(have_body_text('The details of the new account are below. You can approve or reject this application.')) - end - end - - describe '.new_trends' do - let(:recipient) { Fabricate(:account, username: 'Snurf') } - let(:link) { Fabricate(:preview_card, trendable: true, language: 'en') } - let(:status) { Fabricate(:status) } - let(:tag) { Fabricate(:tag) } - let(:mail) { described_class.with(recipient: recipient).new_trends([link], [tag], [status]) } - - before do - PreviewCardTrend.create!(preview_card: link) - StatusTrend.create!(status: status, account: Fabricate(:account)) - recipient.user.update(locale: :en) - end - - it 'renders the email' do - expect(mail) - .to be_present - .and(deliver_to(recipient.user_email)) - .and(deliver_from('notifications@localhost')) - .and(have_subject('New trends up for review on cb6e6126.ngrok.io')) - .and(have_body_text('The following items need a review before they can be displayed publicly')) - .and(have_body_text(ActivityPub::TagManager.instance.url_for(status))) - .and(have_body_text(link.title)) - .and(have_body_text(tag.display_name)) - end - end - - describe '.new_software_updates' do - let(:recipient) { Fabricate(:account, username: 'Bob') } - let(:mail) { described_class.with(recipient: recipient).new_software_updates } - - before do - recipient.user.update(locale: :en) - end - - it 'renders the email' do - expect(mail) - .to be_present - .and(deliver_to(recipient.user_email)) - .and(deliver_from('notifications@localhost')) - .and(have_subject('New Mastodon versions are available for cb6e6126.ngrok.io!')) - .and(have_body_text('New Mastodon versions have been released, you may want to update!')) - end - end - - describe '.new_critical_software_updates' do - let(:recipient) { Fabricate(:account, username: 'Bob') } - let(:mail) { described_class.with(recipient: recipient).new_critical_software_updates } - - before do - recipient.user.update(locale: :en) - end - - it 'renders the email' do - expect(mail) - .to be_present - .and(deliver_to(recipient.user_email)) - .and(deliver_from('notifications@localhost')) - .and(have_subject('Critical Mastodon updates are available for cb6e6126.ngrok.io!')) - .and(have_body_text('New critical versions of Mastodon have been released, you may want to update as soon as possible!')) - .and(have_header('Importance', 'high')) - .and(have_header('Priority', 'urgent')) - .and(have_header('X-Priority', '1')) - end - end - - describe '.auto_close_registrations' do - let(:recipient) { Fabricate(:account, username: 'Bob') } - let(:mail) { described_class.with(recipient: recipient).auto_close_registrations } - - before do - recipient.user.update(locale: :en) - end - - it 'renders the email' do - expect(mail) - .to be_present - .and(deliver_to(recipient.user_email)) - .and(deliver_from('notifications@localhost')) - .and(have_subject('Registrations for cb6e6126.ngrok.io have been automatically switched to requiring approval')) - .and(have_body_text('have been automatically switched')) - end - end -end diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb deleted file mode 100644 index eab196166d..0000000000 --- a/spec/mailers/notification_mailer_spec.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe NotificationMailer do - let(:receiver) { Fabricate(:user, account_attributes: { username: 'alice' }) } - let(:sender) { Fabricate(:account, username: 'bob') } - let(:foreign_status) { Fabricate(:status, account: sender, text: 'The body of the foreign status') } - let(:own_status) { Fabricate(:status, account: receiver.account, text: 'The body of the own status') } - - shared_examples 'standard headers' do |type| - it 'renders the email' do - expect(mail) - .to be_present - .and(have_header('To', "#{receiver.account.username} <#{receiver.email}>")) - .and(have_header('List-ID', "<#{type}.alice.cb6e6126.ngrok.io>")) - .and(have_header('List-Unsubscribe', %r{})) - .and(have_header('List-Unsubscribe', /&type=#{type}/)) - .and(have_header('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click')) - .and(deliver_to("#{receiver.account.username} <#{receiver.email}>")) - .and(deliver_from('notifications@localhost')) - end - end - - shared_examples 'thread headers' do - it 'renders the email with conversation thread headers' do - conversation_header_regex = // - expect(mail) - .to be_present - .and(have_header('In-Reply-To', conversation_header_regex)) - .and(have_header('References', conversation_header_regex)) - end - end - - describe 'mention' do - let(:mention) { Mention.create!(account: receiver.account, status: foreign_status) } - let(:notification) { Notification.create!(account: receiver.account, activity: mention) } - let(:mail) { prepared_mailer_for(receiver.account).mention } - - include_examples 'localized subject', 'notification_mailer.mention.subject', name: 'bob' - include_examples 'standard headers', 'mention' - include_examples 'thread headers' - - it 'renders the email' do - expect(mail) - .to be_present - .and(have_subject('You were mentioned by bob')) - .and(have_body_text('You were mentioned by bob')) - .and(have_body_text('The body of the foreign status')) - end - end - - describe 'follow' do - let(:follow) { sender.follow!(receiver.account) } - let(:notification) { Notification.create!(account: receiver.account, activity: follow) } - let(:mail) { prepared_mailer_for(receiver.account).follow } - - include_examples 'localized subject', 'notification_mailer.follow.subject', name: 'bob' - include_examples 'standard headers', 'follow' - - it 'renders the email' do - expect(mail) - .to be_present - .and(have_subject('bob is now following you')) - .and(have_body_text('bob is now following you')) - end - end - - describe 'favourite' do - let(:favourite) { Favourite.create!(account: sender, status: own_status) } - let(:notification) { Notification.create!(account: receiver.account, activity: favourite) } - let(:mail) { prepared_mailer_for(own_status.account).favourite } - - include_examples 'localized subject', 'notification_mailer.favourite.subject', name: 'bob' - include_examples 'standard headers', 'favourite' - include_examples 'thread headers' - - it 'renders the email' do - expect(mail) - .to be_present - .and(have_subject('bob favorited your post')) - .and(have_body_text('Your post was favorited by bob')) - .and(have_body_text('The body of the own status')) - end - end - - describe 'reblog' do - let(:reblog) { Status.create!(account: sender, reblog: own_status) } - let(:notification) { Notification.create!(account: receiver.account, activity: reblog) } - let(:mail) { prepared_mailer_for(own_status.account).reblog } - - include_examples 'localized subject', 'notification_mailer.reblog.subject', name: 'bob' - include_examples 'standard headers', 'reblog' - include_examples 'thread headers' - - it 'renders the email' do - expect(mail) - .to be_present - .and(have_subject('bob boosted your post')) - .and(have_body_text('Your post was boosted by bob')) - .and(have_body_text('The body of the own status')) - end - end - - describe 'follow_request' do - let(:follow_request) { Fabricate(:follow_request, account: sender, target_account: receiver.account) } - let(:notification) { Notification.create!(account: receiver.account, activity: follow_request) } - let(:mail) { prepared_mailer_for(receiver.account).follow_request } - - include_examples 'localized subject', 'notification_mailer.follow_request.subject', name: 'bob' - include_examples 'standard headers', 'follow_request' - - it 'renders the email' do - expect(mail) - .to be_present - .and(have_subject('Pending follower: bob')) - .and(have_body_text('bob has requested to follow you')) - end - end - - private - - def prepared_mailer_for(recipient) - described_class.with(recipient: recipient, notification: notification) - end -end diff --git a/spec/mailers/previews/admin_mailer_preview.rb b/spec/mailers/previews/admin_mailer_preview.rb deleted file mode 100644 index b8fb387acd..0000000000 --- a/spec/mailers/previews/admin_mailer_preview.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -# Preview all emails at http://localhost:3000/rails/mailers/admin_mailer - -class AdminMailerPreview < ActionMailer::Preview - # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_report - def new_report - AdminMailer.with(recipient: Account.first).new_report(Report.first) - end - - # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_appeal - def new_appeal - AdminMailer.with(recipient: Account.first).new_appeal(Appeal.first) - end - - # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_pending_account - def new_pending_account - AdminMailer.with(recipient: Account.first).new_pending_account(User.pending.first) - end - - # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_trends - def new_trends - AdminMailer.with(recipient: Account.first).new_trends(PreviewCard.joins(:trend).limit(3), Tag.limit(3), Status.joins(:trend).where(reblog_of_id: nil).limit(3)) - end - - # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_software_updates - def new_software_updates - AdminMailer.with(recipient: Account.first).new_software_updates - end - - # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_critical_software_updates - def new_critical_software_updates - AdminMailer.with(recipient: Account.first).new_critical_software_updates - end - - # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/auto_close_registrations - def auto_close_registrations - AdminMailer.with(recipient: Account.first).auto_close_registrations - end -end diff --git a/spec/mailers/previews/notification_mailer_preview.rb b/spec/mailers/previews/notification_mailer_preview.rb deleted file mode 100644 index a63c20c27c..0000000000 --- a/spec/mailers/previews/notification_mailer_preview.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -# Preview all emails at http://localhost:3000/rails/mailers/notification_mailer - -class NotificationMailerPreview < ActionMailer::Preview - # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/mention - def mention - activity = Mention.last - mailer_for(activity.account, activity).mention - end - - # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/follow - def follow - activity = Follow.last - mailer_for(activity.target_account, activity).follow - end - - # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/follow_request - def follow_request - activity = Follow.last - mailer_for(activity.target_account, activity).follow_request - end - - # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/favourite - def favourite - activity = Favourite.last - mailer_for(activity.status.account, activity).favourite - end - - # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/reblog - def reblog - activity = Status.where.not(reblog_of_id: nil).first - mailer_for(activity.reblog.account, activity).reblog - end - - private - - def mailer_for(account, activity) - NotificationMailer.with( - recipient: account, - notification: Notification.find_by(activity: activity) - ) - end -end diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb deleted file mode 100644 index 2722538e1a..0000000000 --- a/spec/mailers/previews/user_mailer_preview.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -# Preview all emails at http://localhost:3000/rails/mailers/user_mailer - -class UserMailerPreview < ActionMailer::Preview - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/confirmation_instructions - def confirmation_instructions - UserMailer.confirmation_instructions(User.first, 'spec') - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/email_changed - def email_changed - user = User.first - user.unconfirmed_email = 'foo@bar.com' - UserMailer.email_changed(user) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_change - def password_change - UserMailer.password_change(User.first) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/two_factor_disabled - def two_factor_disabled - UserMailer.two_factor_disabled(User.first) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/two_factor_enabled - def two_factor_enabled - UserMailer.two_factor_enabled(User.first) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/two_factor_recovery_codes_changed - def two_factor_recovery_codes_changed - UserMailer.two_factor_recovery_codes_changed(User.first) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/webauthn_enabled - def webauthn_enabled - UserMailer.webauthn_enabled(User.first) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/webauthn_disabled - def webauthn_disabled - UserMailer.webauthn_disabled(User.first) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/webauthn_credential_added - def webauthn_credential_added - webauthn_credential = WebauthnCredential.new(nickname: 'USB Key') - UserMailer.webauthn_credential_added(User.first, webauthn_credential) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/webauthn_credential_deleted - def webauthn_credential_deleted - webauthn_credential = WebauthnCredential.new(nickname: 'USB Key') - UserMailer.webauthn_credential_deleted(User.first, webauthn_credential) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/reconfirmation_instructions - def reconfirmation_instructions - user = User.first - user.unconfirmed_email = 'foo@bar.com' - UserMailer.confirmation_instructions(user, 'spec') - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/reset_password_instructions - def reset_password_instructions - UserMailer.reset_password_instructions(User.first, 'spec') - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/welcome - def welcome - UserMailer.welcome(User.first) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/backup_ready - def backup_ready - UserMailer.backup_ready(User.first, Backup.first) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/warning - def warning - UserMailer.warning(User.first, AccountWarning.last) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/appeal_approved - def appeal_approved - UserMailer.appeal_approved(User.first, Appeal.last) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/suspicious_sign_in - def suspicious_sign_in - UserMailer.suspicious_sign_in(User.first, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc) - end - - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/failed_2fa - def failed_2fa - UserMailer.failed_2fa(User.first, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc) - end -end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb deleted file mode 100644 index 5a8c293740..0000000000 --- a/spec/mailers/user_mailer_spec.rb +++ /dev/null @@ -1,275 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe UserMailer do - let(:receiver) { Fabricate(:user) } - - describe '#confirmation_instructions' do - let(:mail) { described_class.confirmation_instructions(receiver, 'spec') } - - it 'renders confirmation instructions' do - receiver.update!(locale: nil) - - expect(mail) - .to be_present - .and(have_body_text(I18n.t('devise.mailer.confirmation_instructions.title'))) - .and(have_body_text('spec')) - .and(have_body_text(Rails.configuration.x.local_domain)) - end - - include_examples 'localized subject', - 'devise.mailer.confirmation_instructions.subject', - instance: Rails.configuration.x.local_domain - end - - describe '#reconfirmation_instructions' do - let(:mail) { described_class.confirmation_instructions(receiver, 'spec') } - - it 'renders reconfirmation instructions' do - receiver.update!(email: 'new-email@example.com', locale: nil) - - expect(mail) - .to be_present - .and(have_body_text(I18n.t('devise.mailer.reconfirmation_instructions.title'))) - .and(have_body_text('spec')) - .and(have_body_text(Rails.configuration.x.local_domain)) - end - - include_examples 'localized subject', - 'devise.mailer.confirmation_instructions.subject', - instance: Rails.configuration.x.local_domain - end - - describe '#reset_password_instructions' do - let(:mail) { described_class.reset_password_instructions(receiver, 'spec') } - - it 'renders reset password instructions' do - receiver.update!(locale: nil) - - expect(mail) - .to be_present - .and(have_body_text(I18n.t('devise.mailer.reset_password_instructions.title'))) - .and(have_body_text('spec')) - end - - include_examples 'localized subject', - 'devise.mailer.reset_password_instructions.subject' - end - - describe '#password_change' do - let(:mail) { described_class.password_change(receiver) } - - it 'renders password change notification' do - receiver.update!(locale: nil) - - expect(mail) - .to be_present - .and(have_body_text(I18n.t('devise.mailer.password_change.title'))) - end - - include_examples 'localized subject', - 'devise.mailer.password_change.subject' - end - - describe '#email_changed' do - let(:mail) { described_class.email_changed(receiver) } - - it 'renders email change notification' do - receiver.update!(locale: nil) - - expect(mail) - .to be_present - .and(have_body_text(I18n.t('devise.mailer.email_changed.title'))) - end - - include_examples 'localized subject', - 'devise.mailer.email_changed.subject' - end - - describe '#warning' do - let(:strike) { Fabricate(:account_warning, target_account: receiver.account, text: 'dont worry its just the testsuite', action: 'suspend') } - let(:mail) { described_class.warning(receiver, strike) } - - it 'renders warning notification' do - receiver.update!(locale: nil) - - expect(mail) - .to be_present - .and(have_body_text(I18n.t('user_mailer.warning.title.suspend', acct: receiver.account.acct))) - .and(have_body_text(strike.text)) - end - end - - describe '#webauthn_credential_deleted' do - let(:credential) { Fabricate(:webauthn_credential, user_id: receiver.id) } - let(:mail) { described_class.webauthn_credential_deleted(receiver, credential) } - - it 'renders webauthn credential deleted notification' do - receiver.update!(locale: nil) - - expect(mail) - .to be_present - .and(have_body_text(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) { described_class.suspicious_sign_in(receiver, ip, agent, timestamp) } - - it 'renders suspicious sign in notification' do - receiver.update!(locale: nil) - - expect(mail) - .to be_present - .and(have_body_text(I18n.t('user_mailer.suspicious_sign_in.explanation'))) - end - - include_examples 'localized subject', - 'user_mailer.suspicious_sign_in.subject' - end - - describe '#failed_2fa' do - let(:ip) { '192.168.0.1' } - let(:agent) { 'NCSA_Mosaic/2.0 (Windows 3.1)' } - let(:timestamp) { Time.now.utc } - let(:mail) { described_class.failed_2fa(receiver, ip, agent, timestamp) } - - it 'renders failed 2FA notification' do - receiver.update!(locale: nil) - - expect(mail) - .to be_present - .and(have_body_text(I18n.t('user_mailer.failed_2fa.explanation'))) - end - - include_examples 'localized subject', - 'user_mailer.failed_2fa.subject' - end - - describe '#appeal_approved' do - let(:appeal) { Fabricate(:appeal, account: receiver.account, approved_at: Time.now.utc) } - let(:mail) { described_class.appeal_approved(receiver, appeal) } - - it 'renders appeal_approved notification' do - expect(mail) - .to be_present - .and(have_subject(I18n.t('user_mailer.appeal_approved.subject', date: I18n.l(appeal.created_at)))) - .and(have_body_text(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) { described_class.appeal_rejected(receiver, appeal) } - - it 'renders appeal_rejected notification' do - expect(mail) - .to be_present - .and(have_subject(I18n.t('user_mailer.appeal_rejected.subject', date: I18n.l(appeal.created_at)))) - .and(have_body_text(I18n.t('user_mailer.appeal_rejected.title'))) - end - end - - describe '#two_factor_enabled' do - let(:mail) { described_class.two_factor_enabled(receiver) } - - it 'renders two_factor_enabled mail' do - expect(mail) - .to be_present - .and(have_subject(I18n.t('devise.mailer.two_factor_enabled.subject'))) - .and(have_body_text(I18n.t('devise.mailer.two_factor_enabled.explanation'))) - end - end - - describe '#two_factor_disabled' do - let(:mail) { described_class.two_factor_disabled(receiver) } - - it 'renders two_factor_disabled mail' do - expect(mail) - .to be_present - .and(have_subject(I18n.t('devise.mailer.two_factor_disabled.subject'))) - .and(have_body_text(I18n.t('devise.mailer.two_factor_disabled.explanation'))) - end - end - - describe '#webauthn_enabled' do - let(:mail) { described_class.webauthn_enabled(receiver) } - - it 'renders webauthn_enabled mail' do - expect(mail) - .to be_present - .and(have_subject(I18n.t('devise.mailer.webauthn_enabled.subject'))) - .and(have_body_text(I18n.t('devise.mailer.webauthn_enabled.explanation'))) - end - end - - describe '#webauthn_disabled' do - let(:mail) { described_class.webauthn_disabled(receiver) } - - it 'renders webauthn_disabled mail' do - expect(mail) - .to be_present - .and(have_subject(I18n.t('devise.mailer.webauthn_disabled.subject'))) - .and(have_body_text(I18n.t('devise.mailer.webauthn_disabled.explanation'))) - end - end - - describe '#two_factor_recovery_codes_changed' do - let(:mail) { described_class.two_factor_recovery_codes_changed(receiver) } - - it 'renders two_factor_recovery_codes_changed mail' do - expect(mail) - .to be_present - .and(have_subject(I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject'))) - .and(have_body_text(I18n.t('devise.mailer.two_factor_recovery_codes_changed.explanation'))) - end - end - - describe '#webauthn_credential_added' do - let(:credential) { Fabricate.build(:webauthn_credential) } - let(:mail) { described_class.webauthn_credential_added(receiver, credential) } - - it 'renders webauthn_credential_added mail' do - expect(mail) - .to be_present - .and(have_subject(I18n.t('devise.mailer.webauthn_credential.added.subject'))) - .and(have_body_text(I18n.t('devise.mailer.webauthn_credential.added.explanation'))) - end - end - - describe '#welcome' do - let(:mail) { described_class.welcome(receiver) } - - before do - # This is a bit hacky and low-level but this allows stubbing trending tags - tag_ids = Fabricate.times(5, :tag).pluck(:id) - allow(Trends.tags).to receive(:query).and_return(instance_double(Trends::Query, allowed: Tag.where(id: tag_ids))) - end - - it 'renders welcome mail' do - expect(mail) - .to be_present - .and(have_subject(I18n.t('user_mailer.welcome.subject'))) - .and(have_body_text(I18n.t('user_mailer.welcome.explanation'))) - end - end - - describe '#backup_ready' do - let(:backup) { Fabricate(:backup) } - let(:mail) { described_class.backup_ready(receiver, backup) } - - it 'renders backup_ready mail' do - expect(mail) - .to be_present - .and(have_subject(I18n.t('user_mailer.backup_ready.subject'))) - .and(have_body_text(I18n.t('user_mailer.backup_ready.explanation'))) - end - end -end diff --git a/spec/models/account/field_spec.rb b/spec/models/account/field_spec.rb deleted file mode 100644 index 22593bb218..0000000000 --- a/spec/models/account/field_spec.rb +++ /dev/null @@ -1,164 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Account::Field do - describe '#verified?' do - subject { described_class.new(account, 'name' => 'Foo', 'value' => 'Bar', 'verified_at' => verified_at) } - - let(:account) { instance_double(Account, local?: true) } - - context 'when verified_at is set' do - let(:verified_at) { Time.now.utc.iso8601 } - - it 'returns true' do - expect(subject.verified?).to be true - end - end - - context 'when verified_at is not set' do - let(:verified_at) { nil } - - it 'returns false' do - expect(subject.verified?).to be false - end - end - end - - describe '#mark_verified!' do - subject { described_class.new(account, original_hash) } - - let(:account) { instance_double(Account, local?: true) } - let(:original_hash) { { 'name' => 'Foo', 'value' => 'Bar' } } - - before do - subject.mark_verified! - end - - it 'updates verified_at' do - expect(subject.verified_at).to_not be_nil - end - - it 'updates original hash' do - expect(original_hash['verified_at']).to_not be_nil - end - end - - describe '#verifiable?' do - subject { described_class.new(account, 'name' => 'Foo', 'value' => value) } - - let(:account) { instance_double(Account, local?: local) } - - context 'with local accounts' do - let(:local) { true } - - context 'with a URL with misleading authentication' do - let(:value) { 'https://spacex.com @h.43z.one' } - - it 'returns false' do - expect(subject.verifiable?).to be false - end - end - - context 'with a URL' do - let(:value) { 'https://example.com' } - - it 'returns true' do - expect(subject.verifiable?).to be true - end - end - - context 'with an IDN URL' do - let(:value) { 'https://twitter.com∕dougallj∕status∕1590357240443437057.ê.cc/twitter.html' } - - it 'returns false' do - expect(subject.verifiable?).to be false - end - end - - context 'with a URL with a non-normalized path' do - let(:value) { 'https://github.com/octocatxxxxxxxx/../mastodon' } - - it 'returns false' do - expect(subject.verifiable?).to be false - end - end - - context 'with text that is not a URL' do - let(:value) { 'Hello world' } - - it 'returns false' do - expect(subject.verifiable?).to be false - end - end - - context 'with text that contains a URL' do - let(:value) { 'Hello https://example.com world' } - - it 'returns false' do - expect(subject.verifiable?).to be false - end - end - - context 'with text which is blank' do - let(:value) { '' } - - it 'returns false' do - expect(subject.verifiable?).to be false - end - end - end - - context 'with remote accounts' do - let(:local) { false } - - context 'with a link' do - let(:value) { 'patreon.com/mastodon' } - - it 'returns true' do - expect(subject.verifiable?).to be true - end - end - - context 'with a link with misleading authentication' do - let(:value) { 'google.com' } - - it 'returns false' do - expect(subject.verifiable?).to be false - end - end - - context 'with HTML that has more than just a link' do - let(:value) { 'google.com @h.43z.one' } - - it 'returns false' do - expect(subject.verifiable?).to be false - end - end - - context 'with a link with different visible text' do - let(:value) { 'https://example.com/foo' } - - it 'returns false' do - expect(subject.verifiable?).to be false - end - end - - context 'with text that is a URL but is not linked' do - let(:value) { 'https://example.com/foo' } - - it 'returns false' do - expect(subject.verifiable?).to be false - end - end - - context 'with text which is blank' do - let(:value) { '' } - - it 'returns false' do - expect(subject.verifiable?).to be false - end - end - end - end -end diff --git a/spec/models/account_conversation_spec.rb b/spec/models/account_conversation_spec.rb deleted file mode 100644 index 4e8727ca39..0000000000 --- a/spec/models/account_conversation_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AccountConversation do - let!(:alice) { Fabricate(:account, username: 'alice') } - let!(:bob) { Fabricate(:account, username: 'bob') } - let!(:mark) { Fabricate(:account, username: 'mark') } - - describe '.add_status' do - it 'creates new record when no others exist' do - status = Fabricate(:status, account: alice, visibility: :direct) - status.mentions.create(account: bob) - - conversation = described_class.add_status(alice, status) - - expect(conversation.participant_accounts).to include(bob) - expect(conversation.last_status).to eq status - expect(conversation.status_ids).to eq [status.id] - end - - it 'appends to old record when there is a match' do - last_status = Fabricate(:status, account: alice, visibility: :direct) - conversation = described_class.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id]) - - status = Fabricate(:status, account: bob, visibility: :direct, thread: last_status) - status.mentions.create(account: alice) - - new_conversation = described_class.add_status(alice, status) - - expect(new_conversation.id).to eq conversation.id - expect(new_conversation.participant_accounts).to include(bob) - expect(new_conversation.last_status).to eq status - expect(new_conversation.status_ids).to eq [last_status.id, status.id] - end - - it 'creates new record when new participants are added' do - last_status = Fabricate(:status, account: alice, visibility: :direct) - conversation = described_class.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id]) - - status = Fabricate(:status, account: bob, visibility: :direct, thread: last_status) - status.mentions.create(account: alice) - status.mentions.create(account: mark) - - new_conversation = described_class.add_status(alice, status) - - expect(new_conversation.id).to_not eq conversation.id - expect(new_conversation.participant_accounts).to include(bob, mark) - expect(new_conversation.last_status).to eq status - expect(new_conversation.status_ids).to eq [status.id] - end - end - - describe '.remove_status' do - it 'updates last status to a previous value' do - last_status = Fabricate(:status, account: alice, visibility: :direct) - status = Fabricate(:status, account: alice, visibility: :direct) - conversation = described_class.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [status.id, last_status.id]) - last_status.mentions.create(account: bob) - last_status.destroy! - conversation.reload - expect(conversation.last_status).to eq status - expect(conversation.status_ids).to eq [status.id] - end - - it 'removes the record if no other statuses are referenced' do - last_status = Fabricate(:status, account: alice, visibility: :direct) - conversation = described_class.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id]) - last_status.mentions.create(account: bob) - last_status.destroy! - expect(described_class.where(id: conversation.id).count).to eq 0 - end - end -end diff --git a/spec/models/account_domain_block_spec.rb b/spec/models/account_domain_block_spec.rb deleted file mode 100644 index d994403b8e..0000000000 --- a/spec/models/account_domain_block_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AccountDomainBlock do - let(:account) { Fabricate(:account) } - - it 'removes blocking cache after creation' do - Rails.cache.write("exclude_domains_for:#{account.id}", 'a.domain.already.blocked') - - expect { block_domain_for_account('a.domain.blocked.later') } - .to change { account_has_exclude_domains_cache? }.to(false) - end - - it 'removes blocking cache after destruction' do - block = block_domain_for_account('domain') - Rails.cache.write("exclude_domains_for:#{account.id}", 'domain') - - expect { block.destroy! } - .to change { account_has_exclude_domains_cache? }.to(false) - end - - private - - def block_domain_for_account(domain) - Fabricate(:account_domain_block, account: account, domain: domain) - end - - def account_has_exclude_domains_cache? - Rails.cache.exist?("exclude_domains_for:#{account.id}") - end -end diff --git a/spec/models/account_filter_spec.rb b/spec/models/account_filter_spec.rb deleted file mode 100644 index fa47b5954a..0000000000 --- a/spec/models/account_filter_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe AccountFilter do - describe 'with empty params' do - it 'excludes instance actor by default' do - filter = described_class.new({}) - - expect(filter.results).to eq Account.without_instance_actor - end - end - - describe 'with invalid params' do - it 'raises with key error' do - filter = described_class.new(wrong: true) - - expect { filter.results }.to raise_error(/wrong/) - end - end - - describe 'with origin and by_domain interacting' do - let!(:local_account) { Fabricate(:account, domain: nil) } - let!(:remote_account_one) { Fabricate(:account, domain: 'example.org') } - let(:remote_account_two) { Fabricate(:account, domain: 'other.domain') } - - it 'works with domain first and origin remote' do - filter = described_class.new(by_domain: 'example.org', origin: 'remote') - expect(filter.results).to contain_exactly(remote_account_one) - end - - it 'works with domain last and origin remote' do - filter = described_class.new(origin: 'remote', by_domain: 'example.org') - expect(filter.results).to contain_exactly(remote_account_one) - end - - it 'works with domain first and origin local' do - filter = described_class.new(by_domain: 'example.org', origin: 'local') - expect(filter.results).to contain_exactly(local_account) - end - - it 'works with domain last and origin local' do - filter = described_class.new(origin: 'local', by_domain: 'example.org') - expect(filter.results).to contain_exactly(remote_account_one) - end - end - - describe 'with username' do - let!(:local_account) { Fabricate(:account, domain: nil, username: 'validUserName') } - - it 'works with @ at the beginning of the username' do - filter = described_class.new(username: '@validUserName') - expect(filter.results).to contain_exactly(local_account) - end - - it 'does not work with more than one @ at the beginning of the username' do - filter = described_class.new(username: '@@validUserName') - expect(filter.results).to_not contain_exactly(local_account) - end - - it 'does not work with @ outside the beginning of the username' do - filter = described_class.new(username: 'validUserName@') - expect(filter.results).to_not contain_exactly(local_account) - end - end -end diff --git a/spec/models/account_migration_spec.rb b/spec/models/account_migration_spec.rb deleted file mode 100644 index 1f32c6082e..0000000000 --- a/spec/models/account_migration_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AccountMigration do - describe 'validations' do - subject { described_class.new(account: source_account, acct: target_acct) } - - let(:source_account) { Fabricate(:account) } - let(:target_acct) { target_account.acct } - - context 'with valid properties' do - let(:target_account) { Fabricate(:account, username: 'target', domain: 'remote.org') } - - before do - target_account.aliases.create!(acct: source_account.acct) - - service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(service_double) - allow(service_double).to receive(:call).with(target_acct, anything).and_return(target_account) - end - - it 'passes validations' do - expect(subject).to be_valid - end - end - - context 'with unresolvable account' do - let(:target_acct) { 'target@remote' } - - before do - service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(service_double) - allow(service_double).to receive(:call).with(target_acct, anything).and_return(nil) - end - - it 'has errors on acct field' do - expect(subject).to model_have_error_on_field(:acct) - end - end - - context 'with a space in the domain part' do - let(:target_acct) { 'target@remote. org' } - - it 'has errors on acct field' do - expect(subject).to model_have_error_on_field(:acct) - end - end - end -end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb deleted file mode 100644 index 8e5648a0b0..0000000000 --- a/spec/models/account_spec.rb +++ /dev/null @@ -1,1070 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Account do - context 'with an account record' do - subject { Fabricate(:account) } - - let(:bob) { Fabricate(:account, username: 'bob') } - - describe '#suspend!' do - it 'marks the account as suspended and creates a deletion request' do - expect { subject.suspend! } - .to change(subject, :suspended?).from(false).to(true) - .and(change { AccountDeletionRequest.exists?(account: subject) }.from(false).to(true)) - end - - context 'when the account is of a local user' do - subject { local_user_account } - - let!(:local_user_account) { Fabricate(:user, email: 'foo+bar@domain.org').account } - - it 'creates a canonical domain block' do - subject.suspend! - expect(CanonicalEmailBlock.block?(subject.user_email)).to be true - end - - context 'when a canonical domain block already exists for that email' do - before do - Fabricate(:canonical_email_block, email: subject.user_email) - end - - it 'does not raise an error' do - expect { subject.suspend! }.to_not raise_error - end - end - end - end - - describe '#follow!' do - it 'creates a follow' do - follow = subject.follow!(bob) - - expect(follow).to be_instance_of Follow - expect(follow.account).to eq subject - expect(follow.target_account).to eq bob - end - end - - describe '#unfollow!' do - before do - subject.follow!(bob) - end - - it 'destroys a follow' do - unfollow = subject.unfollow!(bob) - - expect(unfollow).to be_instance_of Follow - expect(unfollow.account).to eq subject - expect(unfollow.target_account).to eq bob - expect(unfollow.destroyed?).to be true - end - end - - describe '#following?' do - it 'returns true when the target is followed' do - subject.follow!(bob) - expect(subject.following?(bob)).to be true - end - - it 'returns false if the target is not followed' do - expect(subject.following?(bob)).to be false - end - end - end - - describe '#local?' do - it 'returns true when the account is local' do - account = Fabricate(:account, domain: nil) - expect(account.local?).to be true - end - - it 'returns false when the account is on a different domain' do - account = Fabricate(:account, domain: 'foreign.tld') - expect(account.local?).to be false - end - end - - describe 'Local domain user methods' do - subject { Fabricate(:account, domain: nil, username: 'alice') } - - around do |example| - before = Rails.configuration.x.local_domain - example.run - Rails.configuration.x.local_domain = before - end - - describe '#to_webfinger_s' do - it 'returns a webfinger string for the account' do - Rails.configuration.x.local_domain = 'example.com' - - expect(subject.to_webfinger_s).to eq 'acct:alice@example.com' - end - end - - describe '#local_username_and_domain' do - it 'returns the username and local domain for the account' do - Rails.configuration.x.local_domain = 'example.com' - - expect(subject.local_username_and_domain).to eq 'alice@example.com' - end - end - end - - describe '#acct' do - it 'returns username for local users' do - account = Fabricate(:account, domain: nil, username: 'alice') - expect(account.acct).to eql 'alice' - end - - it 'returns username@domain for foreign users' do - account = Fabricate(:account, domain: 'foreign.tld', username: 'alice') - expect(account.acct).to eql 'alice@foreign.tld' - end - end - - describe '#save_with_optional_media!' do - before do - stub_request(:get, 'https://remote.test/valid_avatar').to_return(request_fixture('avatar.txt')) - stub_request(:get, 'https://remote.test/invalid_avatar').to_return(request_fixture('feed.txt')) - end - - let(:account) do - Fabricate(:account, - avatar_remote_url: 'https://remote.test/valid_avatar', - header_remote_url: 'https://remote.test/valid_avatar') - end - - let!(:expectation) { account.dup } - - context 'with valid properties' do - before do - account.save_with_optional_media! - end - - it 'unchanges avatar, header, avatar_remote_url, and header_remote_url' do - expect(account.avatar_remote_url).to eq expectation.avatar_remote_url - expect(account.header_remote_url).to eq expectation.header_remote_url - expect(account.avatar_file_name).to eq expectation.avatar_file_name - expect(account.header_file_name).to eq expectation.header_file_name - end - end - - context 'with invalid properties' do - before do - account.avatar_remote_url = 'https://remote.test/invalid_avatar' - account.save_with_optional_media! - end - - it 'sets default avatar, header, avatar_remote_url, and header_remote_url' do - expect(account.avatar_remote_url).to eq 'https://remote.test/invalid_avatar' - expect(account.header_remote_url).to eq expectation.header_remote_url - expect(account.avatar_file_name).to be_nil - expect(account.header_file_name).to eq expectation.header_file_name - end - end - end - - describe '#possibly_stale?' do - let(:account) { Fabricate(:account, last_webfingered_at: last_webfingered_at) } - - context 'when last_webfingered_at is nil' do - let(:last_webfingered_at) { nil } - - it 'returns true' do - expect(account.possibly_stale?).to be true - end - end - - context 'when last_webfingered_at is more than 24 hours before' do - let(:last_webfingered_at) { 25.hours.ago } - - it 'returns true' do - expect(account.possibly_stale?).to be true - end - end - - context 'when last_webfingered_at is less than 24 hours before' do - let(:last_webfingered_at) { 23.hours.ago } - - it 'returns false' do - expect(account.possibly_stale?).to be false - end - end - end - - describe '#refresh!' do - let(:account) { Fabricate(:account, domain: domain) } - let(:acct) { account.acct } - - context 'when domain is nil' do - let(:domain) { nil } - - it 'returns nil' do - expect(account.refresh!).to be_nil - end - - it 'does not call ResolveAccountService#call' do - service = instance_double(ResolveAccountService, call: nil) - allow(ResolveAccountService).to receive(:new).and_return(service) - - account.refresh! - - expect(service).to_not have_received(:call).with(acct) - end - end - - context 'when domain is present' do - let(:domain) { 'example.com' } - - it 'calls ResolveAccountService#call' do - service = instance_double(ResolveAccountService, call: nil) - allow(ResolveAccountService).to receive(:new).and_return(service) - - account.refresh! - - expect(service).to have_received(:call).with(acct).once - end - end - end - - describe '#to_param' do - it 'returns username' do - account = Fabricate(:account, username: 'alice') - expect(account.to_param).to eq 'alice' - end - end - - describe '#keypair' do - it 'returns an RSA key pair' do - account = Fabricate(:account) - expect(account.keypair).to be_instance_of OpenSSL::PKey::RSA - end - end - - describe '#object_type' do - it 'is always a person' do - account = Fabricate(:account) - expect(account.object_type).to be :person - end - end - - describe '#favourited?' do - subject { Fabricate(:account) } - - let(:original_status) do - author = Fabricate(:account, username: 'original') - Fabricate(:status, account: author) - end - - context 'when the status is a reblog of another status' do - let(:original_reblog) do - author = Fabricate(:account, username: 'original_reblogger') - Fabricate(:status, reblog: original_status, account: author) - end - - it 'is true when this account has favourited it' do - Fabricate(:favourite, status: original_reblog, account: subject) - - expect(subject.favourited?(original_status)).to be true - end - - it 'is false when this account has not favourited it' do - expect(subject.favourited?(original_status)).to be false - end - end - - context 'when the status is an original status' do - it 'is true when this account has favourited it' do - Fabricate(:favourite, status: original_status, account: subject) - - expect(subject.favourited?(original_status)).to be true - end - - it 'is false when this account has not favourited it' do - expect(subject.favourited?(original_status)).to be false - end - end - end - - describe '#reblogged?' do - subject { Fabricate(:account) } - - let(:original_status) do - author = Fabricate(:account, username: 'original') - Fabricate(:status, account: author) - end - - context 'when the status is a reblog of another status' do - let(:original_reblog) do - author = Fabricate(:account, username: 'original_reblogger') - Fabricate(:status, reblog: original_status, account: author) - end - - it 'is true when this account has reblogged it' do - Fabricate(:status, reblog: original_reblog, account: subject) - - expect(subject.reblogged?(original_reblog)).to be true - end - - it 'is false when this account has not reblogged it' do - expect(subject.reblogged?(original_reblog)).to be false - end - end - - context 'when the status is an original status' do - it 'is true when this account has reblogged it' do - Fabricate(:status, reblog: original_status, account: subject) - - expect(subject.reblogged?(original_status)).to be true - end - - it 'is false when this account has not reblogged it' do - expect(subject.reblogged?(original_status)).to be false - end - end - end - - describe '#excluded_from_timeline_account_ids' do - it 'includes account ids of blockings, blocked_bys and mutes' do - account = Fabricate(:account) - block = Fabricate(:block, account: account) - mute = Fabricate(:mute, account: account) - block_by = Fabricate(:block, target_account: account) - - results = account.excluded_from_timeline_account_ids - expect(results.size).to eq 3 - expect(results).to include( - block.target_account.id, - mute.target_account.id, - block_by.account.id - ) - end - end - - describe '#excluded_from_timeline_domains' do - it 'returns the domains blocked by the account' do - account = Fabricate(:account) - account.block_domain!('domain') - expect(account.excluded_from_timeline_domains).to contain_exactly('domain') - end - end - - describe '.search_for' do - before do - _missing = Fabricate( - :account, - display_name: 'Missing', - username: 'missing', - domain: 'missing.com' - ) - end - - it 'does not return suspended users' do - Fabricate( - :account, - display_name: 'Display Name', - username: 'username', - domain: 'example.com', - suspended: true - ) - - results = described_class.search_for('username') - expect(results).to eq [] - end - - it 'does not return unapproved users' do - match = Fabricate( - :account, - display_name: 'Display Name', - username: 'username' - ) - - match.user.update(approved: false) - - results = described_class.search_for('username') - expect(results).to eq [] - end - - it 'does not return unconfirmed users' do - match = Fabricate( - :account, - display_name: 'Display Name', - username: 'username' - ) - - match.user.update(confirmed_at: nil) - - results = described_class.search_for('username') - expect(results).to eq [] - end - - it 'accepts ?, \, : and space as delimiter' do - match = Fabricate( - :account, - display_name: 'A & l & i & c & e', - username: 'username', - domain: 'example.com' - ) - - results = described_class.search_for('A?l\i:c e') - expect(results).to eq [match] - end - - it 'finds accounts with matching display_name' do - match = Fabricate( - :account, - display_name: 'Display Name', - username: 'username', - domain: 'example.com' - ) - - results = described_class.search_for('display') - expect(results).to eq [match] - end - - it 'finds accounts with matching username' do - match = Fabricate( - :account, - display_name: 'Display Name', - username: 'username', - domain: 'example.com' - ) - - results = described_class.search_for('username') - expect(results).to eq [match] - end - - it 'finds accounts with matching domain' do - match = Fabricate( - :account, - display_name: 'Display Name', - username: 'username', - domain: 'example.com' - ) - - results = described_class.search_for('example') - expect(results).to eq [match] - end - - it 'limits via constant by default' do - stub_const('Account::Search::DEFAULT_LIMIT', 1) - 2.times.each { Fabricate(:account, display_name: 'Display Name') } - results = described_class.search_for('display') - expect(results.size).to eq 1 - end - - it 'accepts arbitrary limits' do - 2.times.each { Fabricate(:account, display_name: 'Display Name') } - results = described_class.search_for('display', limit: 1) - expect(results.size).to eq 1 - end - - it 'ranks multiple matches higher' do - matches = [ - { username: 'username', display_name: 'username' }, - { display_name: 'Display Name', username: 'username', domain: 'example.com' }, - ].map(&method(:Fabricate).curry(2).call(:account)) - - results = described_class.search_for('username') - expect(results).to eq matches - end - end - - describe '.advanced_search_for' do - let(:account) { Fabricate(:account) } - - context 'when limiting search to followed accounts' do - it 'accepts ?, \, : and space as delimiter' do - match = Fabricate( - :account, - display_name: 'A & l & i & c & e', - username: 'username', - domain: 'example.com' - ) - account.follow!(match) - - results = described_class.advanced_search_for('A?l\i:c e', account, limit: 10, following: true) - expect(results).to eq [match] - end - - it 'does not return non-followed accounts' do - Fabricate( - :account, - display_name: 'A & l & i & c & e', - username: 'username', - domain: 'example.com' - ) - - results = described_class.advanced_search_for('A?l\i:c e', account, limit: 10, following: true) - expect(results).to eq [] - end - - it 'does not return suspended users' do - Fabricate( - :account, - display_name: 'Display Name', - username: 'username', - domain: 'example.com', - suspended: true - ) - - results = described_class.advanced_search_for('username', account, limit: 10, following: true) - expect(results).to eq [] - end - - it 'does not return unapproved users' do - match = Fabricate( - :account, - display_name: 'Display Name', - username: 'username' - ) - - match.user.update(approved: false) - - results = described_class.advanced_search_for('username', account, limit: 10, following: true) - expect(results).to eq [] - end - - it 'does not return unconfirmed users' do - match = Fabricate( - :account, - display_name: 'Display Name', - username: 'username' - ) - - match.user.update(confirmed_at: nil) - - results = described_class.advanced_search_for('username', account, limit: 10, following: true) - expect(results).to eq [] - end - end - - it 'does not return suspended users' do - Fabricate( - :account, - display_name: 'Display Name', - username: 'username', - domain: 'example.com', - suspended: true - ) - - results = described_class.advanced_search_for('username', account) - expect(results).to eq [] - end - - it 'does not return unapproved users' do - match = Fabricate( - :account, - display_name: 'Display Name', - username: 'username' - ) - - match.user.update(approved: false) - - results = described_class.advanced_search_for('username', account) - expect(results).to eq [] - end - - it 'does not return unconfirmed users' do - match = Fabricate( - :account, - display_name: 'Display Name', - username: 'username' - ) - - match.user.update(confirmed_at: nil) - - results = described_class.advanced_search_for('username', account) - expect(results).to eq [] - end - - it 'accepts ?, \, : and space as delimiter' do - match = Fabricate( - :account, - display_name: 'A & l & i & c & e', - username: 'username', - domain: 'example.com' - ) - - results = described_class.advanced_search_for('A?l\i:c e', account) - expect(results).to eq [match] - end - - it 'limits result count by default value' do - stub_const('Account::Search::DEFAULT_LIMIT', 1) - 2.times { Fabricate(:account, display_name: 'Display Name') } - results = described_class.advanced_search_for('display', account) - expect(results.size).to eq 1 - end - - it 'accepts arbitrary limits' do - 2.times { Fabricate(:account, display_name: 'Display Name') } - results = described_class.advanced_search_for('display', account, limit: 1) - expect(results.size).to eq 1 - end - - it 'ranks followed accounts higher' do - match = Fabricate(:account, username: 'Matching') - followed_match = Fabricate(:account, username: 'Matcher') - Fabricate(:follow, account: account, target_account: followed_match) - - results = described_class.advanced_search_for('match', account) - expect(results).to eq [followed_match, match] - expect(results.first.rank).to be > results.last.rank - end - end - - describe '#statuses_count' do - subject { Fabricate(:account) } - - it 'counts statuses' do - Fabricate(:status, account: subject) - Fabricate(:status, account: subject) - expect(subject.statuses_count).to eq 2 - end - - it 'does not count direct statuses' do - Fabricate(:status, account: subject, visibility: :direct) - expect(subject.statuses_count).to eq 0 - end - - it 'is decremented when status is removed' do - status = Fabricate(:status, account: subject) - expect(subject.statuses_count).to eq 1 - status.destroy - expect(subject.statuses_count).to eq 0 - end - - it 'is decremented when status is removed when account is not preloaded' do - status = Fabricate(:status, account: subject) - expect(subject.reload.statuses_count).to eq 1 - clean_status = Status.find(status.id) - expect(clean_status.association(:account).loaded?).to be false - clean_status.destroy - expect(subject.reload.statuses_count).to eq 0 - end - end - - describe '.following_map' do - it 'returns an hash' do - expect(described_class.following_map([], 1)).to be_a Hash - end - end - - describe '.followed_by_map' do - it 'returns an hash' do - expect(described_class.followed_by_map([], 1)).to be_a Hash - end - end - - describe '.blocking_map' do - it 'returns an hash' do - expect(described_class.blocking_map([], 1)).to be_a Hash - end - end - - describe '.requested_map' do - it 'returns an hash' do - expect(described_class.requested_map([], 1)).to be_a Hash - end - end - - describe '.requested_by_map' do - it 'returns an hash' do - expect(described_class.requested_by_map([], 1)).to be_a Hash - end - end - - describe 'MENTION_RE' do - subject { described_class::MENTION_RE } - - it 'matches usernames in the middle of a sentence' do - expect(subject.match('Hello to @alice from me')[1]).to eq 'alice' - end - - it 'matches usernames in the beginning of status' do - expect(subject.match('@alice Hey how are you?')[1]).to eq 'alice' - end - - it 'matches full usernames' do - expect(subject.match('@alice@example.com')[1]).to eq 'alice@example.com' - end - - it 'matches full usernames with a dot at the end' do - expect(subject.match('Hello @alice@example.com.')[1]).to eq 'alice@example.com' - end - - it 'matches dot-prepended usernames' do - expect(subject.match('.@alice I want everybody to see this')[1]).to eq 'alice' - end - - it 'does not match e-mails' do - expect(subject.match('Drop me an e-mail at alice@example.com')).to be_nil - end - - it 'does not match URLs' do - expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil - end - - it 'does not match URL query string' do - expect(subject.match('https://example.com/?x=@alice')).to be_nil - end - - it 'matches usernames immediately following the letter ß' do - expect(subject.match('Hello toß @alice from me')[1]).to eq 'alice' - end - - it 'matches usernames containing uppercase characters' do - expect(subject.match('Hello to @aLice@Example.com from me')[1]).to eq 'aLice@Example.com' - end - end - - describe 'validations' do - it 'is invalid without a username' do - account = Fabricate.build(:account, username: nil) - account.valid? - expect(account).to model_have_error_on_field(:username) - end - - it 'squishes the username before validation' do - account = Fabricate(:account, domain: nil, username: " \u3000bob \t \u00a0 \n ") - expect(account.username).to eq 'bob' - end - - context 'when is local' do - it 'is invalid if the username is not unique in case-insensitive comparison among local accounts' do - _account = Fabricate(:account, username: 'the_doctor') - non_unique_account = Fabricate.build(:account, username: 'the_Doctor') - non_unique_account.valid? - expect(non_unique_account).to model_have_error_on_field(:username) - end - - it 'is invalid if the username is reserved' do - account = Fabricate.build(:account, username: 'support') - account.valid? - expect(account).to model_have_error_on_field(:username) - end - - it 'is valid when username is reserved but record has already been created' do - account = Fabricate.build(:account, username: 'support') - account.save(validate: false) - expect(account.valid?).to be true - end - - it 'is valid if we are creating an instance actor account with a period' do - account = Fabricate.build(:account, id: described_class::INSTANCE_ACTOR_ID, actor_type: 'Application', locked: true, username: 'example.com') - expect(account.valid?).to be true - end - - it 'is valid if we are creating a possibly-conflicting instance actor account' do - _account = Fabricate(:account, username: 'examplecom') - instance_account = Fabricate.build(:account, id: described_class::INSTANCE_ACTOR_ID, actor_type: 'Application', locked: true, username: 'example.com') - expect(instance_account.valid?).to be true - end - - it 'is invalid if the username doesn\'t only contains letters, numbers and underscores' do - account = Fabricate.build(:account, username: 'the-doctor') - account.valid? - expect(account).to model_have_error_on_field(:username) - end - - it 'is invalid if the username contains a period' do - account = Fabricate.build(:account, username: 'the.doctor') - account.valid? - expect(account).to model_have_error_on_field(:username) - end - - it 'is invalid if the username is longer than the character limit' do - account = Fabricate.build(:account, username: username_over_limit) - account.valid? - expect(account).to model_have_error_on_field(:username) - end - - it 'is invalid if the display name is longer than the character limit' do - account = Fabricate.build(:account, display_name: display_name_over_limit) - account.valid? - expect(account).to model_have_error_on_field(:display_name) - end - - it 'is invalid if the note is longer than the character limit' do - account = Fabricate.build(:account, note: account_note_over_limit) - account.valid? - expect(account).to model_have_error_on_field(:note) - end - end - - context 'when is remote' do - it 'is invalid if the username is same among accounts in the same normalized domain' do - Fabricate(:account, domain: 'にゃん', username: 'username') - account = Fabricate.build(:account, domain: 'xn--r9j5b5b', username: 'username') - account.valid? - expect(account).to model_have_error_on_field(:username) - end - - it 'is invalid if the username is not unique in case-insensitive comparison among accounts in the same normalized domain' do - Fabricate(:account, domain: 'にゃん', username: 'username') - account = Fabricate.build(:account, domain: 'xn--r9j5b5b', username: 'Username') - account.valid? - expect(account).to model_have_error_on_field(:username) - end - - it 'is valid even if the username contains hyphens' do - account = Fabricate.build(:account, domain: 'domain', username: 'the-doctor') - account.valid? - expect(account).to_not model_have_error_on_field(:username) - end - - it 'is invalid if the username doesn\'t only contains letters, numbers, underscores and hyphens' do - account = Fabricate.build(:account, domain: 'domain', username: 'the doctor') - account.valid? - expect(account).to model_have_error_on_field(:username) - end - - it 'is valid even if the username is longer than the character limit' do - account = Fabricate.build(:account, domain: 'domain', username: username_over_limit) - account.valid? - expect(account).to_not model_have_error_on_field(:username) - end - - it 'is valid even if the display name is longer than the character limit' do - account = Fabricate.build(:account, domain: 'domain', display_name: display_name_over_limit) - account.valid? - expect(account).to_not model_have_error_on_field(:display_name) - end - - it 'is valid even if the note is longer than the character limit' do - account = Fabricate.build(:account, domain: 'domain', note: account_note_over_limit) - account.valid? - expect(account).to_not model_have_error_on_field(:note) - end - end - - def username_over_limit - 'a' * described_class::USERNAME_LENGTH_LIMIT * 2 - end - - def display_name_over_limit - 'a' * described_class::DISPLAY_NAME_LENGTH_LIMIT * 2 - end - - def account_note_over_limit - 'a' * described_class::NOTE_LENGTH_LIMIT * 2 - end - end - - describe 'scopes' do - describe 'matches_uri_prefix' do - let!(:alice) { Fabricate :account, domain: 'host.example', uri: 'https://host.example/user/a' } - let!(:bob) { Fabricate :account, domain: 'top-level.example', uri: 'https://top-level.example' } - - it 'returns accounts which start with the value' do - results = described_class.matches_uri_prefix('https://host.example') - - expect(results.size) - .to eq(1) - expect(results) - .to include(alice) - .and not_include(bob) - end - - it 'returns accounts which equal the value' do - results = described_class.matches_uri_prefix('https://top-level.example') - - expect(results.size) - .to eq(1) - expect(results) - .to include(bob) - .and not_include(alice) - end - end - - describe 'auditable' do - let!(:alice) { Fabricate :account } - let!(:bob) { Fabricate :account } - - before do - 2.times { Fabricate :action_log, account: alice } - end - - it 'returns distinct accounts with action log records' do - results = described_class.auditable - - expect(results.size) - .to eq(1) - expect(results) - .to include(alice) - .and not_include(bob) - end - end - - describe 'alphabetic' do - it 'sorts by alphabetic order of domain and username' do - matches = [ - { username: 'a', domain: 'a' }, - { username: 'b', domain: 'a' }, - { username: 'a', domain: 'b' }, - { username: 'b', domain: 'b' }, - ].map(&method(:Fabricate).curry(2).call(:account)) - - expect(described_class.without_internal.alphabetic).to eq matches - end - end - - describe 'matches_display_name' do - it 'matches display name which starts with the given string' do - match = Fabricate(:account, display_name: 'pattern and suffix') - Fabricate(:account, display_name: 'prefix and pattern') - - expect(described_class.matches_display_name('pattern')).to eq [match] - end - end - - describe 'matches_username' do - it 'matches display name which starts with the given string' do - match = Fabricate(:account, username: 'pattern_and_suffix') - Fabricate(:account, username: 'prefix_and_pattern') - - expect(described_class.matches_username('pattern')).to eq [match] - end - end - - describe 'by_domain_and_subdomains' do - it 'returns exact domain matches' do - account = Fabricate(:account, domain: 'example.com') - expect(described_class.by_domain_and_subdomains('example.com')).to eq [account] - end - - it 'returns subdomains' do - account = Fabricate(:account, domain: 'foo.example.com') - expect(described_class.by_domain_and_subdomains('example.com')).to eq [account] - end - - it 'does not return partially matching domains' do - account = Fabricate(:account, domain: 'grexample.com') - expect(described_class.by_domain_and_subdomains('example.com')).to_not eq [account] - end - end - - describe 'remote' do - it 'returns an array of accounts who have a domain' do - _account = Fabricate(:account, domain: nil) - account_with_domain = Fabricate(:account, domain: 'example.com') - expect(described_class.remote).to contain_exactly(account_with_domain) - end - end - - describe 'local' do - it 'returns an array of accounts who do not have a domain' do - local_account = Fabricate(:account, domain: nil) - _account_with_domain = Fabricate(:account, domain: 'example.com') - expect(described_class.without_internal.local).to contain_exactly(local_account) - end - end - - describe 'partitioned' do - it 'returns a relation of accounts partitioned by domain' do - matches = %w(a b a b) - matches.size.times.to_a.shuffle.each do |index| - matches[index] = Fabricate(:account, domain: matches[index]) - end - - expect(described_class.without_internal.partitioned).to match_array(matches) - end - end - - describe 'recent' do - it 'returns a relation of accounts sorted by recent creation' do - matches = Array.new(2) { Fabricate(:account) } - expect(described_class.without_internal.recent).to match_array(matches) - end - end - - describe 'silenced' do - it 'returns an array of accounts who are silenced' do - silenced_account = Fabricate(:account, silenced: true) - _account = Fabricate(:account, silenced: false) - expect(described_class.silenced).to contain_exactly(silenced_account) - end - end - - describe 'suspended' do - it 'returns an array of accounts who are suspended' do - suspended_account = Fabricate(:account, suspended: true) - _account = Fabricate(:account, suspended: false) - expect(described_class.suspended).to contain_exactly(suspended_account) - end - end - - describe 'searchable' do - let!(:suspended_local) { Fabricate(:account, suspended: true, username: 'suspended_local') } - let!(:suspended_remote) { Fabricate(:account, suspended: true, domain: 'example.org', username: 'suspended_remote') } - let!(:silenced_local) { Fabricate(:account, silenced: true, username: 'silenced_local') } - let!(:silenced_remote) { Fabricate(:account, silenced: true, domain: 'example.org', username: 'silenced_remote') } - let!(:unconfirmed) { Fabricate(:user, confirmed_at: nil).account } - let!(:unapproved) { Fabricate(:user, approved: false).account } - let!(:unconfirmed_unapproved) { Fabricate(:user, confirmed_at: nil, approved: false).account } - let!(:local_account) { Fabricate(:account, username: 'local_account') } - let!(:remote_account) { Fabricate(:account, domain: 'example.org', username: 'remote_account') } - - before do - # Accounts get automatically-approved depending on settings, so ensure they aren't approved - unapproved.user.update(approved: false) - unconfirmed_unapproved.user.update(approved: false) - end - - it 'returns every usable non-suspended account' do - expect(described_class.searchable).to contain_exactly(silenced_local, silenced_remote, local_account, remote_account) - expect(described_class.searchable).to_not include(suspended_local, suspended_remote, unconfirmed, unapproved) - end - - it 'does not mess with previously-applied scopes' do - expect(described_class.where.not(id: remote_account.id).searchable).to contain_exactly(silenced_local, silenced_remote, local_account) - end - end - end - - context 'when is local' do - it 'generates keys' do - account = described_class.create!(domain: nil, username: 'user_without_keys') - - expect(account) - .to be_private_key - .and be_public_key - expect(account.keypair) - .to be_private - .and be_public - end - end - - context 'when is remote' do - it 'does not generate keys' do - key = OpenSSL::PKey::RSA.new(1024).public_key - account = described_class.create!(domain: 'remote', uri: 'https://remote/actor', username: 'remote_user_with_public', public_key: key.to_pem) - expect(account.keypair.params).to eq key.params - end - - it 'normalizes domain' do - account = described_class.create!(domain: 'にゃん', uri: 'https://xn--r9j5b5b/actor', username: 'remote_user_with_idn_domain') - expect(account.domain).to eq 'xn--r9j5b5b' - end - end - - include_examples 'AccountAvatar', :account - include_examples 'AccountHeader', :account - - describe '#increment_count!' do - subject { Fabricate(:account) } - - it 'increments the count in multi-threaded an environment when account_stat is not yet initialized' do - subject - - multi_threaded_execution(15) do - described_class.find(subject.id).increment_count!(:followers_count) - end - - expect(subject.reload.followers_count).to eq 15 - end - end -end diff --git a/spec/models/account_statuses_cleanup_policy_spec.rb b/spec/models/account_statuses_cleanup_policy_spec.rb deleted file mode 100644 index a08fd723a4..0000000000 --- a/spec/models/account_statuses_cleanup_policy_spec.rb +++ /dev/null @@ -1,504 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AccountStatusesCleanupPolicy do - let(:account) { Fabricate(:account, username: 'alice', domain: nil) } - - describe 'validation' do - it 'disallow remote accounts' do - account.update(domain: 'example.com') - account_statuses_cleanup_policy = Fabricate.build(:account_statuses_cleanup_policy, account: account) - account_statuses_cleanup_policy.valid? - expect(account_statuses_cleanup_policy).to model_have_error_on_field(:account) - end - end - - describe 'save hooks' do - context 'when widening a policy' do - let!(:account_statuses_cleanup_policy) do - Fabricate(:account_statuses_cleanup_policy, - account: account, - keep_direct: true, - keep_pinned: true, - keep_polls: true, - keep_media: true, - keep_self_fav: true, - keep_self_bookmark: true, - min_favs: 1, - min_reblogs: 1) - end - - before do - account_statuses_cleanup_policy.record_last_inspected(42) - end - - it 'invalidates last_inspected when widened because of keep_direct' do - account_statuses_cleanup_policy.keep_direct = false - account_statuses_cleanup_policy.save - expect(account_statuses_cleanup_policy.last_inspected).to be_nil - end - - it 'invalidates last_inspected when widened because of keep_pinned' do - account_statuses_cleanup_policy.keep_pinned = false - account_statuses_cleanup_policy.save - expect(account_statuses_cleanup_policy.last_inspected).to be_nil - end - - it 'invalidates last_inspected when widened because of keep_polls' do - account_statuses_cleanup_policy.keep_polls = false - account_statuses_cleanup_policy.save - expect(account_statuses_cleanup_policy.last_inspected).to be_nil - end - - it 'invalidates last_inspected when widened because of keep_media' do - account_statuses_cleanup_policy.keep_media = false - account_statuses_cleanup_policy.save - expect(account_statuses_cleanup_policy.last_inspected).to be_nil - end - - it 'invalidates last_inspected when widened because of keep_self_fav' do - account_statuses_cleanup_policy.keep_self_fav = false - account_statuses_cleanup_policy.save - expect(account_statuses_cleanup_policy.last_inspected).to be_nil - end - - it 'invalidates last_inspected when widened because of keep_self_bookmark' do - account_statuses_cleanup_policy.keep_self_bookmark = false - account_statuses_cleanup_policy.save - expect(account_statuses_cleanup_policy.last_inspected).to be_nil - end - - it 'invalidates last_inspected when widened because of higher min_favs' do - account_statuses_cleanup_policy.min_favs = 5 - account_statuses_cleanup_policy.save - expect(account_statuses_cleanup_policy.last_inspected).to be_nil - end - - it 'invalidates last_inspected when widened because of disabled min_favs' do - account_statuses_cleanup_policy.min_favs = nil - account_statuses_cleanup_policy.save - expect(account_statuses_cleanup_policy.last_inspected).to be_nil - end - - it 'invalidates last_inspected when widened because of higher min_reblogs' do - account_statuses_cleanup_policy.min_reblogs = 5 - account_statuses_cleanup_policy.save - expect(account_statuses_cleanup_policy.last_inspected).to be_nil - end - - it 'invalidates last_inspected when widened because of disable min_reblogs' do - account_statuses_cleanup_policy.min_reblogs = nil - account_statuses_cleanup_policy.save - expect(account_statuses_cleanup_policy.last_inspected).to be_nil - end - end - - context 'when narrowing a policy' do - let!(:account_statuses_cleanup_policy) do - Fabricate(:account_statuses_cleanup_policy, - account: account, - keep_direct: false, - keep_pinned: false, - keep_polls: false, - keep_media: false, - keep_self_fav: false, - keep_self_bookmark: false, - min_favs: nil, - min_reblogs: nil) - end - - it 'does not unnecessarily invalidate last_inspected' do - account_statuses_cleanup_policy.record_last_inspected(42) - account_statuses_cleanup_policy.keep_direct = true - account_statuses_cleanup_policy.keep_pinned = true - account_statuses_cleanup_policy.keep_polls = true - account_statuses_cleanup_policy.keep_media = true - account_statuses_cleanup_policy.keep_self_fav = true - account_statuses_cleanup_policy.keep_self_bookmark = true - account_statuses_cleanup_policy.min_favs = 5 - account_statuses_cleanup_policy.min_reblogs = 5 - account_statuses_cleanup_policy.save - expect(account_statuses_cleanup_policy.last_inspected).to eq 42 - end - end - end - - describe '#record_last_inspected' do - let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } - - it 'records the given id' do - account_statuses_cleanup_policy.record_last_inspected(42) - expect(account_statuses_cleanup_policy.last_inspected).to eq 42 - end - end - - describe '#invalidate_last_inspected' do - subject { account_statuses_cleanup_policy.invalidate_last_inspected(status, action) } - - let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } - let(:status) { Fabricate(:status, id: 10, account: account) } - - before do - account_statuses_cleanup_policy.record_last_inspected(42) - end - - context 'when the action is :unbookmark' do - let(:action) { :unbookmark } - - context 'when the policy is not to keep self-bookmarked toots' do - before do - account_statuses_cleanup_policy.keep_self_bookmark = false - end - - it 'does not change the recorded id' do - subject - expect(account_statuses_cleanup_policy.last_inspected).to eq 42 - end - end - - context 'when the policy is to keep self-bookmarked toots' do - before do - account_statuses_cleanup_policy.keep_self_bookmark = true - end - - it 'records the older id' do - subject - expect(account_statuses_cleanup_policy.last_inspected).to eq 10 - end - end - end - - context 'when the action is :unfav' do - let(:action) { :unfav } - - context 'when the policy is not to keep self-favourited toots' do - before do - account_statuses_cleanup_policy.keep_self_fav = false - end - - it 'does not change the recorded id' do - subject - expect(account_statuses_cleanup_policy.last_inspected).to eq 42 - end - end - - context 'when the policy is to keep self-favourited toots' do - before do - account_statuses_cleanup_policy.keep_self_fav = true - end - - it 'records the older id' do - subject - expect(account_statuses_cleanup_policy.last_inspected).to eq 10 - end - end - end - - context 'when the action is :unpin' do - let(:action) { :unpin } - - context 'when the policy is not to keep pinned toots' do - before do - account_statuses_cleanup_policy.keep_pinned = false - end - - it 'does not change the recorded id' do - subject - expect(account_statuses_cleanup_policy.last_inspected).to eq 42 - end - end - - context 'when the policy is to keep pinned toots' do - before do - account_statuses_cleanup_policy.keep_pinned = true - end - - it 'records the older id' do - subject - expect(account_statuses_cleanup_policy.last_inspected).to eq 10 - end - end - end - - context 'when the status is more recent than the recorded inspected id' do - let(:action) { :unfav } - let(:status) { Fabricate(:status, account: account) } - - it 'does not change the recorded id' do - subject - expect(account_statuses_cleanup_policy.last_inspected).to eq 42 - end - end - end - - describe '#compute_cutoff_id' do - subject { account_statuses_cleanup_policy.compute_cutoff_id } - - let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } - - before { Fabricate(:status, created_at: 3.years.ago) } - - context 'when the account has posted multiple toots' do - let!(:old_status) { Fabricate(:status, created_at: 3.weeks.ago, account: account) } - - before do - Fabricate(:status, created_at: 3.years.ago, account: account) - Fabricate(:status, created_at: 2.days.ago, account: account) - end - - it 'returns the most recent id that is still below policy age' do - expect(subject).to eq old_status.id - end - end - - context 'when the account has not posted anything' do - it 'returns nil' do - expect(subject).to be_nil - end - end - end - - describe '#statuses_to_delete' do - subject { account_statuses_cleanup_policy.statuses_to_delete } - - let!(:unrelated_status) { Fabricate(:status, created_at: 3.years.ago) } - let!(:very_old_status) { Fabricate(:status, created_at: 3.years.ago, account: account) } - let!(:pinned_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } - let!(:direct_message) { Fabricate(:status, created_at: 1.year.ago, account: account, visibility: :direct) } - let!(:self_faved) { Fabricate(:status, created_at: 1.year.ago, account: account) } - let!(:self_bookmarked) { Fabricate(:status, created_at: 1.year.ago, account: account) } - let!(:status_with_poll) { Fabricate(:status, created_at: 1.year.ago, account: account, poll_attributes: { account: account, voters_count: 0, options: %w(a b), expires_in: 2.days }) } - let!(:status_with_media) { Fabricate(:status, created_at: 1.year.ago, account: account) } - let!(:faved_primary) { Fabricate(:status, created_at: 1.year.ago, account: account) } - let!(:faved_secondary) { Fabricate(:status, created_at: 1.year.ago, account: account) } - let!(:reblogged_primary) { Fabricate(:status, created_at: 1.year.ago, account: account) } - let!(:reblogged_secondary) { Fabricate(:status, created_at: 1.year.ago, account: account) } - let!(:recent_status) { Fabricate(:status, created_at: 2.days.ago, account: account) } - - let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } - - before do - Fabricate(:media_attachment, account: account, status: status_with_media) - Fabricate(:status_pin, account: account, status: pinned_status) - Fabricate(:favourite, account: account, status: self_faved) - Fabricate(:bookmark, account: account, status: self_bookmarked) - - faved_primary.status_stat.update(favourites_count: 4) - faved_secondary.status_stat.update(favourites_count: 5) - reblogged_primary.status_stat.update(reblogs_count: 4) - reblogged_secondary.status_stat.update(reblogs_count: 5) - end - - context 'when passed a max_id' do - subject { account_statuses_cleanup_policy.statuses_to_delete(50, old_status.id).pluck(:id) } - - let!(:old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } - let!(:slightly_less_old_status) { Fabricate(:status, created_at: 6.months.ago, account: account) } - - it 'returns statuses included the max_id and older than the max_id but not newer than max_id' do - expect(subject) - .to include(old_status.id) - .and include(very_old_status.id) - .and not_include(slightly_less_old_status.id) - end - end - - context 'when passed a min_id' do - subject { account_statuses_cleanup_policy.statuses_to_delete(50, recent_status.id, old_status.id).pluck(:id) } - - let!(:old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } - let!(:slightly_less_old_status) { Fabricate(:status, created_at: 6.months.ago, account: account) } - - it 'returns statuses including min_id and newer than min_id, but not older than min_id' do - expect(subject) - .to include(old_status.id) - .and include(slightly_less_old_status.id) - .and not_include(very_old_status.id) - end - end - - context 'when passed a low limit' do - it 'only returns the limited number of items' do - expect(account_statuses_cleanup_policy.statuses_to_delete(1).count).to eq 1 - end - end - - context 'when policy is set to keep statuses more recent than 2 years' do - before do - account_statuses_cleanup_policy.min_status_age = 2.years.seconds - end - - it 'does not return unrelated old status and does return oldest status' do - expect(subject.pluck(:id)) - .to not_include(unrelated_status.id) - .and eq [very_old_status.id] - end - end - - context 'when policy is set to keep DMs and reject everything else' do - before do - account_statuses_cleanup_policy.keep_direct = true - account_statuses_cleanup_policy.keep_pinned = false - account_statuses_cleanup_policy.keep_polls = false - account_statuses_cleanup_policy.keep_media = false - account_statuses_cleanup_policy.keep_self_fav = false - account_statuses_cleanup_policy.keep_self_bookmark = false - end - - it 'returns every old status except does not return the old direct message for deletion' do - expect(subject.pluck(:id)) - .to not_include(direct_message.id) - .and include(very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) - end - end - - context 'when policy is set to keep self-bookmarked toots and reject everything else' do - before do - account_statuses_cleanup_policy.keep_direct = false - account_statuses_cleanup_policy.keep_pinned = false - account_statuses_cleanup_policy.keep_polls = false - account_statuses_cleanup_policy.keep_media = false - account_statuses_cleanup_policy.keep_self_fav = false - account_statuses_cleanup_policy.keep_self_bookmark = true - end - - it 'returns every old status but does not return the old self-bookmarked message for deletion' do - expect(subject.pluck(:id)) - .to not_include(self_bookmarked.id) - .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) - end - end - - context 'when policy is set to keep self-faved toots and reject everything else' do - before do - account_statuses_cleanup_policy.keep_direct = false - account_statuses_cleanup_policy.keep_pinned = false - account_statuses_cleanup_policy.keep_polls = false - account_statuses_cleanup_policy.keep_media = false - account_statuses_cleanup_policy.keep_self_fav = true - account_statuses_cleanup_policy.keep_self_bookmark = false - end - - it 'returns every old status but does not return the old self-faved message for deletion' do - expect(subject.pluck(:id)) - .to not_include(self_faved.id) - .and include(direct_message.id, very_old_status.id, pinned_status.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) - end - end - - context 'when policy is set to keep toots with media and reject everything else' do - before do - account_statuses_cleanup_policy.keep_direct = false - account_statuses_cleanup_policy.keep_pinned = false - account_statuses_cleanup_policy.keep_polls = false - account_statuses_cleanup_policy.keep_media = true - account_statuses_cleanup_policy.keep_self_fav = false - account_statuses_cleanup_policy.keep_self_bookmark = false - end - - it 'returns every old status but does not return the old message with media for deletion' do - expect(subject.pluck(:id)) - .to not_include(status_with_media.id) - .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) - end - end - - context 'when policy is set to keep toots with polls and reject everything else' do - before do - account_statuses_cleanup_policy.keep_direct = false - account_statuses_cleanup_policy.keep_pinned = false - account_statuses_cleanup_policy.keep_polls = true - account_statuses_cleanup_policy.keep_media = false - account_statuses_cleanup_policy.keep_self_fav = false - account_statuses_cleanup_policy.keep_self_bookmark = false - end - - it 'returns every old status but does not return the old poll message for deletion' do - expect(subject.pluck(:id)) - .to not_include(status_with_poll.id) - .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) - end - end - - context 'when policy is set to keep pinned toots and reject everything else' do - before do - account_statuses_cleanup_policy.keep_direct = false - account_statuses_cleanup_policy.keep_pinned = true - account_statuses_cleanup_policy.keep_polls = false - account_statuses_cleanup_policy.keep_media = false - account_statuses_cleanup_policy.keep_self_fav = false - account_statuses_cleanup_policy.keep_self_bookmark = false - end - - it 'returns every old status but does not return the old pinned message for deletion' do - expect(subject.pluck(:id)) - .to not_include(pinned_status.id) - .and include(direct_message.id, very_old_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) - end - end - - context 'when policy is to not keep any special messages' do - before do - account_statuses_cleanup_policy.keep_direct = false - account_statuses_cleanup_policy.keep_pinned = false - account_statuses_cleanup_policy.keep_polls = false - account_statuses_cleanup_policy.keep_media = false - account_statuses_cleanup_policy.keep_self_fav = false - account_statuses_cleanup_policy.keep_self_bookmark = false - end - - it 'returns every old status but does not return the recent or unrelated statuses' do - expect(subject.pluck(:id)) - .to not_include(recent_status.id) - .and not_include(unrelated_status.id) - .and include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) - end - end - - context 'when policy is set to keep every category of toots' do - before do - account_statuses_cleanup_policy.keep_direct = true - account_statuses_cleanup_policy.keep_pinned = true - account_statuses_cleanup_policy.keep_polls = true - account_statuses_cleanup_policy.keep_media = true - account_statuses_cleanup_policy.keep_self_fav = true - account_statuses_cleanup_policy.keep_self_bookmark = true - end - - it 'returns normal statuses and does not return unrelated old status' do - expect(subject.pluck(:id)) - .to not_include(unrelated_status.id) - .and contain_exactly(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id, reblogged_secondary.id) - end - end - - context 'when policy is to keep statuses with at least 5 boosts' do - before do - account_statuses_cleanup_policy.min_reblogs = 5 - end - - it 'returns old not-reblogged statuses but does not return the recent, 5-times reblogged, or unrelated statuses' do - expect(subject.pluck(:id)) - .to not_include(recent_status.id) - .and not_include(reblogged_secondary.id) - .and not_include(unrelated_status.id) - .and include(very_old_status.id, faved_primary.id, faved_secondary.id, reblogged_primary.id) - end - end - - context 'when policy is to keep statuses with at least 5 favs' do - before do - account_statuses_cleanup_policy.min_favs = 5 - end - - it 'returns old not-faved statuses but does not return the recent, 5-times faved, or unrelated statuses' do - expect(subject.pluck(:id)) - .to not_include(recent_status.id) - .and not_include(faved_secondary.id) - .and not_include(unrelated_status.id) - .and include(very_old_status.id, faved_primary.id, reblogged_primary.id, reblogged_secondary.id) - end - end - end -end diff --git a/spec/models/account_suggestions/friends_of_friends_source_spec.rb b/spec/models/account_suggestions/friends_of_friends_source_spec.rb deleted file mode 100644 index c2f8d0f86c..0000000000 --- a/spec/models/account_suggestions/friends_of_friends_source_spec.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AccountSuggestions::FriendsOfFriendsSource do - describe '#get' do - subject { described_class.new } - - let!(:bob) { Fabricate(:account, discoverable: true, hide_collections: false) } - let!(:alice) { Fabricate(:account, discoverable: true, hide_collections: true) } - let!(:eve) { Fabricate(:account, discoverable: true, hide_collections: false) } - let!(:mallory) { Fabricate(:account, discoverable: false, hide_collections: false) } - let!(:eugen) { Fabricate(:account, discoverable: true, hide_collections: false) } - let!(:neil) { Fabricate(:account, discoverable: true, hide_collections: false) } - let!(:john) { Fabricate(:account, discoverable: true, hide_collections: false) } - let!(:jerk) { Fabricate(:account, discoverable: true, hide_collections: false) } - let!(:larry) { Fabricate(:account, discoverable: true, hide_collections: false) } - - context 'with follows and blocks' do - before do - bob.block!(jerk) - FollowRecommendationMute.create!(account: bob, target_account: neil) - - # bob follows eugen, alice and larry - [eugen, alice, larry].each { |account| bob.follow!(account) } - - # alice follows eve and mallory - [john, mallory].each { |account| alice.follow!(account) } - - # eugen follows eve, john, jerk, larry and neil - [eve, mallory, jerk, larry, neil].each { |account| eugen.follow!(account) } - end - - it 'returns eligible accounts', :aggregate_failures do - results = subject.get(bob) - - # eve is returned through eugen - expect(results).to include([eve.id, :friends_of_friends]) - - # john is not reachable because alice hides who she follows - expect(results).to_not include([john.id, :friends_of_friends]) - - # mallory is not discoverable - expect(results).to_not include([mallory.id, :friends_of_friends]) - - # larry is not included because he's followed already - expect(results).to_not include([larry.id, :friends_of_friends]) - - # jerk is blocked - expect(results).to_not include([jerk.id, :friends_of_friends]) - - # the suggestion for neil has already been rejected - expect(results).to_not include([neil.id, :friends_of_friends]) - end - end - - context 'with deterministic order' do - before do - # bob follows eve and mallory - [eve, mallory].each { |account| bob.follow!(account) } - - # eve follows eugen, john, and jerk - [jerk, eugen, john].each { |account| eve.follow!(account) } - - # mallory follows eugen, john, and neil - [neil, eugen, john].each { |account| mallory.follow!(account) } - - john.follow!(eugen) - john.follow!(neil) - end - - it 'returns eligible accounts in the expected order' do - expect(subject.get(bob)).to eq expected_results - end - - it 'contains correct underlying source data' do - expect(source_query_values) - .to contain_exactly( - [john.id, 2, 2], # Followed by 2 friends of bob (eve, mallory), 2 followers total (breaks tie) - [eugen.id, 2, 3], # Followed by 2 friends of bob (eve, mallory), 3 followers total - [jerk.id, 1, 1], # Followed by 1 friends of bob (eve), 1 followers total (breaks tie) - [neil.id, 1, 2] # Followed by 1 friends of bob (mallory), 2 followers total - ) - end - - def expected_results - [ - [john.id, :friends_of_friends], - [eugen.id, :friends_of_friends], - [jerk.id, :friends_of_friends], - [neil.id, :friends_of_friends], - ] - end - - def source_query_values - subject.source_query(bob).to_a - end - end - end -end diff --git a/spec/models/account_suggestions/source_spec.rb b/spec/models/account_suggestions/source_spec.rb deleted file mode 100644 index 1666094082..0000000000 --- a/spec/models/account_suggestions/source_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AccountSuggestions::Source do - describe '#base_account_scope' do - subject { FakeSource.new } - - before do - stub_const 'FakeSource', fake_source_class - end - - context 'with follows and follow requests' do - let!(:account_domain_blocked_account) { Fabricate(:account, domain: 'blocked.host', discoverable: true) } - let!(:account) { Fabricate(:account, discoverable: true) } - let!(:blocked_account) { Fabricate(:account, discoverable: true) } - let!(:eligible_account) { Fabricate(:account, discoverable: true) } - let!(:follow_recommendation_muted_account) { Fabricate(:account, discoverable: true) } - let!(:follow_requested_account) { Fabricate(:account, discoverable: true) } - let!(:following_account) { Fabricate(:account, discoverable: true) } - let!(:moved_account) { Fabricate(:account, moved_to_account: Fabricate(:account), discoverable: true) } - let!(:silenced_account) { Fabricate(:account, silenced: true, discoverable: true) } - let!(:undiscoverable_account) { Fabricate(:account, discoverable: false) } - - before do - Fabricate :account_domain_block, account: account, domain: account_domain_blocked_account.domain - Fabricate :block, account: account, target_account: blocked_account - Fabricate :follow_recommendation_mute, account: account, target_account: follow_recommendation_muted_account - Fabricate :follow_request, account: account, target_account: follow_requested_account - Fabricate :follow, account: account, target_account: following_account - end - - it 'returns eligible accounts' do - results = subject.get(account) - - expect(results) - .to include(eligible_account) - .and not_include(account_domain_blocked_account) - .and not_include(account) - .and not_include(blocked_account) - .and not_include(follow_recommendation_muted_account) - .and not_include(follow_requested_account) - .and not_include(following_account) - .and not_include(moved_account) - .and not_include(silenced_account) - .and not_include(undiscoverable_account) - end - end - end - - private - - def fake_source_class - Class.new described_class do - def get(account, limit: 10) - base_account_scope(account) - .limit(limit) - end - end - end -end diff --git a/spec/models/account_warning_preset_spec.rb b/spec/models/account_warning_preset_spec.rb deleted file mode 100644 index f171df7c97..0000000000 --- a/spec/models/account_warning_preset_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# 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/admin/account_action_spec.rb b/spec/models/admin/account_action_spec.rb deleted file mode 100644 index 49bc2b4a91..0000000000 --- a/spec/models/admin/account_action_spec.rb +++ /dev/null @@ -1,152 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Admin::AccountAction do - let(:account_action) { described_class.new } - - describe '#save!' do - subject { account_action.save! } - - let(:account) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:target_account) { Fabricate(:account) } - let(:type) { 'disable' } - - before do - account_action.assign_attributes( - type: type, - current_account: account, - target_account: target_account - ) - end - - context 'when type is "disable"' do - let(:type) { 'disable' } - - it 'disable user' do - subject - expect(target_account.user).to be_disabled - end - end - - context 'when type is "silence"' do - let(:type) { 'silence' } - - it 'silences account' do - subject - expect(target_account).to be_silenced - end - end - - context 'when type is "suspend"' do - let(:type) { 'suspend' } - - it 'suspends account' do - subject - expect(target_account).to be_suspended - end - - it 'queues Admin::SuspensionWorker by 1' do - expect do - subject - end.to change { Admin::SuspensionWorker.jobs.size }.by 1 - end - end - - context 'when type is invalid' do - let(:type) { 'whatever' } - - it 'raises an invalid record error' do - expect { subject }.to raise_error(ActiveRecord::RecordInvalid) - end - end - - context 'when type is not given' do - let(:type) { '' } - - it 'raises an invalid record error' do - expect { subject }.to raise_error(ActiveRecord::RecordInvalid) - end - end - - it 'sends email to target account user', :inline_jobs do - emails = capture_emails { subject } - - expect(emails).to contain_exactly( - have_attributes( - to: contain_exactly(target_account.user.email) - ) - ) - end - - it 'sends notification, log the action, and closes other reports', :aggregate_failures do - other_report = Fabricate(:report, target_account: target_account) - - expect { subject } - .to (change(Admin::ActionLog.where(action: type), :count).by 1) - .and(change { other_report.reload.action_taken? }.from(false).to(true)) - - expect(LocalNotificationWorker).to have_enqueued_sidekiq_job(target_account.id, anything, 'AccountWarning', 'moderation_warning') - end - end - - describe '#report' do - subject { account_action.report } - - context 'with report_id.present?' do - before do - account_action.report_id = Fabricate(:report).id - end - - it 'returns Report' do - expect(subject).to be_instance_of Report - end - end - - context 'with !report_id.present?' do - it 'returns nil' do - expect(subject).to be_nil - end - end - end - - describe '#with_report?' do - subject { account_action.with_report? } - - context 'with !report.nil?' do - before do - account_action.report_id = Fabricate(:report).id - end - - it 'returns true' do - expect(subject).to be true - end - end - - context 'with !(!report.nil?)' do - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '.types_for_account' do - subject { described_class.types_for_account(account) } - - context 'when Account.local?' do - let(:account) { Fabricate(:account, domain: nil) } - - it 'returns ["none", "disable", "sensitive", "silence", "suspend"]' do - expect(subject).to eq %w(none disable sensitive silence suspend) - end - end - - context 'with !account.local?' do - let(:account) { Fabricate(:account, domain: 'hoge.com') } - - it 'returns ["sensitive", "silence", "suspend"]' do - expect(subject).to eq %w(sensitive silence suspend) - end - end - end -end diff --git a/spec/models/admin/action_log_spec.rb b/spec/models/admin/action_log_spec.rb deleted file mode 100644 index 1e3649b833..0000000000 --- a/spec/models/admin/action_log_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Admin::ActionLog do - describe '#action' do - it 'returns action' do - action_log = described_class.new(action: 'hoge') - expect(action_log.action).to be :hoge - end - end -end diff --git a/spec/models/admin/appeal_filter_spec.rb b/spec/models/admin/appeal_filter_spec.rb deleted file mode 100644 index e840bc3bc1..0000000000 --- a/spec/models/admin/appeal_filter_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Admin::AppealFilter do - describe '#results' do - let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) } - let(:not_approved_appeal) { Fabricate(:appeal, approved_at: nil) } - - it 'returns filtered appeals' do - filter = described_class.new(status: 'approved') - - expect(filter.results).to eq([approved_appeal]) - end - end -end diff --git a/spec/models/admin/tag_filter_spec.rb b/spec/models/admin/tag_filter_spec.rb deleted file mode 100644 index 21dc28affb..0000000000 --- a/spec/models/admin/tag_filter_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Admin::TagFilter do - describe 'with invalid params' do - it 'raises with key error' do - filter = described_class.new(wrong: true) - - expect { filter.results }.to raise_error(/wrong/) - end - - it 'raises with status scope error' do - filter = described_class.new(status: 'unknown') - - expect { filter.results }.to raise_error(/Unknown status: unknown/) - end - - it 'raises with order value error' do - filter = described_class.new(order: 'unknown') - - expect { filter.results }.to raise_error(/Unknown order: unknown/) - end - end - - describe '#results' do - let(:listable_tag) { Fabricate(:tag, name: 'test1', listable: true) } - let(:not_listable_tag) { Fabricate(:tag, name: 'test2', listable: false) } - - it 'returns tags filtered by name' do - filter = described_class.new(name: 'test') - - expect(filter.results).to eq([listable_tag, not_listable_tag]) - end - end -end diff --git a/spec/models/announcement_spec.rb b/spec/models/announcement_spec.rb deleted file mode 100644 index 1e7283ca77..0000000000 --- a/spec/models/announcement_spec.rb +++ /dev/null @@ -1,177 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Announcement do - describe 'Scopes' do - context 'with published and unpublished records' do - let!(:published) { Fabricate(:announcement, published: true) } - let!(:unpublished) { Fabricate(:announcement, published: false, scheduled_at: 10.days.from_now) } - - describe '#unpublished' do - it 'returns records with published false' do - results = described_class.unpublished - - expect(results).to eq([unpublished]) - end - end - - describe '#published' do - it 'returns records with published true' do - results = described_class.published - - expect(results).to eq([published]) - end - end - end - - context 'with timestamped announcements' do - let!(:adam_announcement) { Fabricate(:announcement, starts_at: 100.days.ago, scheduled_at: 10.days.ago, published_at: 10.days.ago, ends_at: 5.days.from_now) } - let!(:brenda_announcement) { Fabricate(:announcement, starts_at: 10.days.ago, scheduled_at: 100.days.ago, published_at: 10.days.ago, ends_at: 5.days.from_now) } - let!(:clara_announcement) { Fabricate(:announcement, starts_at: 10.days.ago, scheduled_at: 10.days.ago, published_at: 100.days.ago, ends_at: 5.days.from_now) } - let!(:darnelle_announcement) { Fabricate(:announcement, starts_at: 10.days.ago, scheduled_at: 10.days.ago, published_at: 10.days.ago, ends_at: 5.days.from_now, created_at: 100.days.ago) } - - describe '#chronological' do - it 'orders the records correctly' do - results = described_class.chronological - - expect(results).to eq( - [ - adam_announcement, - brenda_announcement, - clara_announcement, - darnelle_announcement, - ] - ) - end - end - - describe '#reverse_chronological' do - it 'orders the records correctly' do - results = described_class.reverse_chronological - - expect(results).to eq( - [ - darnelle_announcement, - clara_announcement, - brenda_announcement, - adam_announcement, - ] - ) - end - end - end - end - - describe 'Validations' do - describe 'text' do - it 'validates presence of attribute' do - record = Fabricate.build(:announcement, text: nil) - - expect(record).to_not be_valid - expect(record.errors[:text]).to be_present - end - end - - describe 'ends_at' do - it 'validates presence when starts_at is present' do - record = Fabricate.build(:announcement, starts_at: 1.day.ago) - - expect(record).to_not be_valid - expect(record.errors[:ends_at]).to be_present - end - - it 'does not validate presence when starts_at is missing' do - record = Fabricate.build(:announcement, starts_at: nil) - - expect(record).to be_valid - expect(record.errors[:ends_at]).to_not be_present - end - end - end - - describe '#publish!' do - it 'publishes an unpublished record' do - announcement = Fabricate(:announcement, published: false, scheduled_at: 10.days.from_now) - - announcement.publish! - - expect(announcement).to be_published - expect(announcement.published_at).to_not be_nil - expect(announcement.scheduled_at).to be_nil - end - end - - describe '#unpublish!' do - it 'unpublishes a published record' do - announcement = Fabricate(:announcement, published: true) - - announcement.unpublish! - - expect(announcement).to_not be_published - expect(announcement.scheduled_at).to be_nil - end - end - - describe '#reactions' do - context 'with announcement_reactions present' do - let(:account_reaction_emoji) { Fabricate :custom_emoji } - let(:other_reaction_emoji) { Fabricate :custom_emoji } - let!(:account) { Fabricate(:account) } - let!(:announcement) { Fabricate(:announcement) } - - before do - Fabricate(:announcement_reaction, announcement: announcement, created_at: 10.days.ago, name: other_reaction_emoji.shortcode) - Fabricate(:announcement_reaction, announcement: announcement, created_at: 5.days.ago, account: account, name: account_reaction_emoji.shortcode) - Fabricate(:announcement_reaction) # For some other announcement - end - - it 'returns the announcement reactions for the announcement' do - results = announcement.reactions - - expect(results).to have_attributes( - size: eq(2), - first: have_attributes(name: other_reaction_emoji.shortcode, me: false), - last: have_attributes(name: account_reaction_emoji.shortcode, me: false) - ) - end - - it 'returns the announcement reactions for the announcement with `me` set correctly' do - results = announcement.reactions(account) - - expect(results).to have_attributes( - size: eq(2), - first: have_attributes(name: other_reaction_emoji.shortcode, me: false), - last: have_attributes(name: account_reaction_emoji.shortcode, me: true) - ) - end - end - end - - describe '#statuses' do - let(:announcement) { Fabricate(:announcement, status_ids: status_ids) } - - context 'with empty status_ids' do - let(:status_ids) { nil } - - it 'returns empty array' do - results = announcement.statuses - - expect(results).to eq([]) - end - end - - context 'with relevant status_ids' do - let(:status) { Fabricate(:status, visibility: :public) } - let(:direct_status) { Fabricate(:status, visibility: :direct) } - let(:status_ids) { [status.id, direct_status.id] } - - it 'returns public and unlisted statuses' do - results = announcement.statuses - - expect(results).to include(status) - expect(results).to_not include(direct_status) - end - end - end -end diff --git a/spec/models/appeal_spec.rb b/spec/models/appeal_spec.rb deleted file mode 100644 index 13ca3a2d90..0000000000 --- a/spec/models/appeal_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Appeal do - describe 'Validations' do - it 'validates text length is under limit' do - appeal = Fabricate.build( - :appeal, - strike: Fabricate(:account_warning), - text: 'a' * described_class::TEXT_LENGTH_LIMIT * 2 - ) - - expect(appeal).to_not be_valid - expect(appeal).to model_have_error_on_field(:text) - end - end - - 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 deleted file mode 100644 index 8249503c59..0000000000 --- a/spec/models/block_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Block do - describe 'validations' do - it 'is invalid without an account' do - block = Fabricate.build(:block, account: nil) - block.valid? - expect(block).to model_have_error_on_field(:account) - end - - it 'is invalid without a target_account' do - block = Fabricate.build(:block, target_account: nil) - block.valid? - expect(block).to model_have_error_on_field(:target_account) - end - end - - it 'removes blocking cache after creation' do - account = Fabricate(:account) - target_account = Fabricate(:account) - Rails.cache.write("exclude_account_ids_for:#{account.id}", []) - Rails.cache.write("exclude_account_ids_for:#{target_account.id}", []) - - described_class.create!(account: account, target_account: target_account) - - expect(Rails.cache.exist?("exclude_account_ids_for:#{account.id}")).to be false - expect(Rails.cache.exist?("exclude_account_ids_for:#{target_account.id}")).to be false - end - - it 'removes blocking cache after destruction' do - account = Fabricate(:account) - target_account = Fabricate(:account) - block = described_class.create!(account: account, target_account: target_account) - Rails.cache.write("exclude_account_ids_for:#{account.id}", [target_account.id]) - Rails.cache.write("exclude_account_ids_for:#{target_account.id}", [account.id]) - - block.destroy! - - expect(Rails.cache.exist?("exclude_account_ids_for:#{account.id}")).to be false - expect(Rails.cache.exist?("exclude_account_ids_for:#{target_account.id}")).to be false - end -end diff --git a/spec/models/canonical_email_block_spec.rb b/spec/models/canonical_email_block_spec.rb deleted file mode 100644 index c63483f968..0000000000 --- a/spec/models/canonical_email_block_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe CanonicalEmailBlock do - describe '#email=' do - let(:target_hash) { '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b' } - - it 'sets canonical_email_hash' do - subject.email = 'test@example.com' - expect(subject.canonical_email_hash).to eq target_hash - end - - it 'sets the same hash even with dot permutations' do - subject.email = 't.e.s.t@example.com' - expect(subject.canonical_email_hash).to eq target_hash - end - - it 'sets the same hash even with extensions' do - subject.email = 'test+mastodon1@example.com' - expect(subject.canonical_email_hash).to eq target_hash - end - - it 'sets the same hash with different casing' do - subject.email = 'Test@EXAMPLE.com' - expect(subject.canonical_email_hash).to eq target_hash - end - end - - describe '.block?' do - before { Fabricate(:canonical_email_block, email: 'foo@bar.com') } - - it 'returns true for the same email' do - expect(described_class.block?('foo@bar.com')).to be true - end - - it 'returns true for the same email with dots' do - expect(described_class.block?('f.oo@bar.com')).to be true - end - - it 'returns true for the same email with extensions' do - expect(described_class.block?('foo+spam@bar.com')).to be true - end - - it 'returns false for different email' do - expect(described_class.block?('hoge@bar.com')).to be false - end - end -end diff --git a/spec/models/concerns/account/counters_spec.rb b/spec/models/concerns/account/counters_spec.rb deleted file mode 100644 index ccac9e95de..0000000000 --- a/spec/models/concerns/account/counters_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Account::Counters do - let!(:account) { Fabricate(:account) } - - describe '#increment_count!' do - let(:increment_by) { 15 } - - it 'increments the count' do - expect(account.followers_count).to eq 0 - account.increment_count!(:followers_count) - expect(account.followers_count).to eq 1 - end - - it 'increments the count in multi-threaded an environment' do - multi_threaded_execution(increment_by) do - account.increment_count!(:statuses_count) - end - - expect(account.statuses_count).to eq increment_by - end - end - - describe '#decrement_count!' do - let(:decrement_by) { 10 } - - it 'decrements the count' do - account.followers_count = 15 - account.save! - expect(account.followers_count).to eq 15 - account.decrement_count!(:followers_count) - expect(account.followers_count).to eq 14 - end - - it 'decrements the count in multi-threaded an environment' do - account.statuses_count = 15 - account.save! - - multi_threaded_execution(decrement_by) do - account.decrement_count!(:statuses_count) - end - - expect(account.statuses_count).to eq 5 - end - - it 'preserves last_status_at when decrementing statuses_count' do - account_stat = Fabricate( - :account_stat, - account: account, - last_status_at: 3.days.ago, - statuses_count: 10 - ) - - expect { account.decrement_count!(:statuses_count) } - .to change(account_stat.reload, :statuses_count).by(-1) - .and not_change(account_stat.reload, :last_status_at) - end - end -end diff --git a/spec/models/concerns/account/finder_concern_spec.rb b/spec/models/concerns/account/finder_concern_spec.rb deleted file mode 100644 index ab5149e987..0000000000 --- a/spec/models/concerns/account/finder_concern_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Account::FinderConcern do - describe 'local finders' do - let!(:account) { Fabricate(:account, username: 'Alice') } - - describe '.find_local' do - it 'returns case-insensitive result' do - expect(Account.find_local('alice')).to eq(account) - end - - it 'returns correctly cased result' do - expect(Account.find_local('Alice')).to eq(account) - end - - it 'returns nil without a match' do - expect(Account.find_local('a_ice')).to be_nil - end - - it 'returns nil for regex style username value' do - expect(Account.find_local('al%')).to be_nil - end - - it 'returns nil for nil username value' do - expect(Account.find_local(nil)).to be_nil - end - - it 'returns nil for blank username value' do - expect(Account.find_local('')).to be_nil - end - end - - describe '.find_local!' do - it 'returns matching result' do - expect(Account.find_local!('alice')).to eq(account) - end - - it 'raises on non-matching result' do - expect { Account.find_local!('missing') }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'raises with blank username' do - expect { Account.find_local!('') }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'raises with nil username' do - expect { Account.find_local!(nil) }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end - - describe 'remote finders' do - let!(:account) { Fabricate(:account, username: 'Alice', domain: 'mastodon.social') } - - describe '.find_remote' do - it 'returns exact match result' do - expect(Account.find_remote('alice', 'mastodon.social')).to eq(account) - end - - it 'returns case-insensitive result' do - expect(Account.find_remote('ALICE', 'MASTODON.SOCIAL')).to eq(account) - end - - it 'returns nil when username does not match' do - expect(Account.find_remote('a_ice', 'mastodon.social')).to be_nil - end - - it 'returns nil when domain does not match' do - expect(Account.find_remote('alice', 'm_stodon.social')).to be_nil - end - - it 'returns nil for regex style domain value' do - expect(Account.find_remote('alice', 'm%')).to be_nil - end - - it 'returns nil for nil username value' do - expect(Account.find_remote(nil, 'domain')).to be_nil - end - - it 'returns nil for blank username value' do - expect(Account.find_remote('', 'domain')).to be_nil - end - end - - describe '.find_remote!' do - it 'returns matching result' do - expect(Account.find_remote!('alice', 'mastodon.social')).to eq(account) - end - - it 'raises on non-matching result' do - expect { Account.find_remote!('missing', 'mastodon.host') }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'raises with blank username' do - expect { Account.find_remote!('', '') }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'raises with nil username' do - expect { Account.find_remote!(nil, nil) }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end -end diff --git a/spec/models/concerns/account/interactions_spec.rb b/spec/models/concerns/account/interactions_spec.rb deleted file mode 100644 index 798a8672da..0000000000 --- a/spec/models/concerns/account/interactions_spec.rb +++ /dev/null @@ -1,759 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Account::Interactions do - let(:account) { Fabricate(:account, username: 'account') } - let(:account_id) { account.id } - let(:account_ids) { [account_id] } - let(:target_account) { Fabricate(:account, username: 'target') } - let(:target_account_id) { target_account.id } - let(:target_account_ids) { [target_account_id] } - - describe '.following_map' do - subject { Account.following_map(target_account_ids, account_id) } - - context 'when Account with Follow' do - it 'returns { target_account_id => true }' do - Fabricate(:follow, account: account, target_account: target_account) - expect(subject).to eq(target_account_id => { reblogs: true, notify: false, languages: nil }) - end - end - - context 'when Account with Follow but with reblogs disabled' do - it 'returns { target_account_id => { reblogs: false } }' do - Fabricate(:follow, account: account, target_account: target_account, show_reblogs: false) - expect(subject).to eq(target_account_id => { reblogs: false, notify: false, languages: nil }) - end - end - - context 'when Account without Follow' do - it 'returns {}' do - expect(subject).to eq({}) - end - end - end - - describe '.followed_by_map' do - subject { Account.followed_by_map(target_account_ids, account_id) } - - context 'when Account with Follow' do - it 'returns { target_account_id => true }' do - Fabricate(:follow, account: target_account, target_account: account) - expect(subject).to eq(target_account_id => true) - end - end - - context 'when Account without Follow' do - it 'returns {}' do - expect(subject).to eq({}) - end - end - end - - describe '.blocking_map' do - subject { Account.blocking_map(target_account_ids, account_id) } - - context 'when Account with Block' do - it 'returns { target_account_id => true }' do - Fabricate(:block, account: account, target_account: target_account) - expect(subject).to eq(target_account_id => true) - end - end - - context 'when Account without Block' do - it 'returns {}' do - expect(subject).to eq({}) - end - end - end - - describe '.muting_map' do - subject { Account.muting_map(target_account_ids, account_id) } - - context 'when Account with Mute' do - before do - Fabricate(:mute, target_account: target_account, account: account, hide_notifications: hide) - end - - context 'when Mute#hide_notifications?' do - let(:hide) { true } - - it 'returns { target_account_id => { notifications: true } }' do - expect(subject).to eq(target_account_id => { notifications: true }) - end - end - - context 'when not Mute#hide_notifications?' do - let(:hide) { false } - - it 'returns { target_account_id => { notifications: false } }' do - expect(subject).to eq(target_account_id => { notifications: false }) - end - end - end - - context 'when Account without Mute' do - it 'returns {}' do - expect(subject).to eq({}) - end - end - end - - describe '#follow!' do - it 'creates and returns Follow' do - expect do - expect(account.follow!(target_account)).to be_a Follow - end.to change { account.following.count }.by 1 - end - end - - describe '#block' do - it 'creates and returns Block' do - expect do - expect(account.block!(target_account)).to be_a Block - end.to change { account.block_relationships.count }.by 1 - end - end - - describe '#mute!' do - subject { account.mute!(target_account, notifications: arg_notifications) } - - context 'when Mute does not exist yet' do - context 'when arg :notifications is nil' do - let(:arg_notifications) { nil } - - it 'creates Mute, and returns Mute' do - expect do - expect(subject).to be_a Mute - end.to change { account.mute_relationships.count }.by 1 - end - end - - context 'when arg :notifications is false' do - let(:arg_notifications) { false } - - it 'creates Mute, and returns Mute' do - expect do - expect(subject).to be_a Mute - end.to change { account.mute_relationships.count }.by 1 - end - end - - context 'when arg :notifications is true' do - let(:arg_notifications) { true } - - it 'creates Mute, and returns Mute' do - expect do - expect(subject).to be_a Mute - end.to change { account.mute_relationships.count }.by 1 - end - end - end - - context 'when Mute already exists' do - before do - account.mute_relationships << mute - end - - let(:mute) do - Fabricate(:mute, - account: account, - target_account: target_account, - hide_notifications: hide_notifications) - end - - context 'when mute.hide_notifications is true' do - let(:hide_notifications) { true } - - context 'when arg :notifications is nil' do - let(:arg_notifications) { nil } - - it 'returns Mute without updating mute.hide_notifications' do - expect do - expect(subject).to be_a Mute - end.to_not change { mute.reload.hide_notifications? }.from(true) - end - end - - context 'when arg :notifications is false' do - let(:arg_notifications) { false } - - it 'returns Mute, and updates mute.hide_notifications false' do - expect do - expect(subject).to be_a Mute - end.to change { mute.reload.hide_notifications? }.from(true).to(false) - end - end - - context 'when arg :notifications is true' do - let(:arg_notifications) { true } - - it 'returns Mute without updating mute.hide_notifications' do - expect do - expect(subject).to be_a Mute - end.to_not change { mute.reload.hide_notifications? }.from(true) - end - end - end - - context 'when mute.hide_notifications is false' do - let(:hide_notifications) { false } - - context 'when arg :notifications is nil' do - let(:arg_notifications) { nil } - - it 'returns Mute, and updates mute.hide_notifications true' do - expect do - expect(subject).to be_a Mute - end.to change { mute.reload.hide_notifications? }.from(false).to(true) - end - end - - context 'when arg :notifications is false' do - let(:arg_notifications) { false } - - it 'returns Mute without updating mute.hide_notifications' do - expect do - expect(subject).to be_a Mute - end.to_not change { mute.reload.hide_notifications? }.from(false) - end - end - - context 'when arg :notifications is true' do - let(:arg_notifications) { true } - - it 'returns Mute, and updates mute.hide_notifications true' do - expect do - expect(subject).to be_a Mute - end.to change { mute.reload.hide_notifications? }.from(false).to(true) - end - end - end - end - end - - describe '#mute_conversation!' do - subject { account.mute_conversation!(conversation) } - - let(:conversation) { Fabricate(:conversation) } - - it 'creates and returns ConversationMute' do - expect do - expect(subject).to be_a ConversationMute - end.to change { account.conversation_mutes.count }.by 1 - end - end - - describe '#block_domain!' do - subject { account.block_domain!(domain) } - - let(:domain) { 'example.com' } - - it 'creates and returns AccountDomainBlock' do - expect do - expect(subject).to be_a AccountDomainBlock - end.to change { account.domain_blocks.count }.by 1 - end - end - - describe '#block_idna_domain!' do - subject do - [ - account.block_domain!(idna_domain), - account.block_domain!(punycode_domain), - ] - end - - let(:idna_domain) { '대한민국.한국' } - let(:punycode_domain) { 'xn--3e0bs9hfvinn1a.xn--3e0b707e' } - - it 'creates single AccountDomainBlock' do - expect do - expect(subject).to all(be_a AccountDomainBlock) - end.to change { account.domain_blocks.count }.by 1 - end - end - - describe '#unfollow!' do - subject { account.unfollow!(target_account) } - - context 'when following target_account' do - it 'returns destroyed Follow' do - account.active_relationships.create(target_account: target_account) - expect(subject).to be_a Follow - expect(subject).to be_destroyed - end - end - - context 'when not following target_account' do - it 'returns nil' do - expect(subject).to be_nil - end - end - end - - describe '#unblock!' do - subject { account.unblock!(target_account) } - - context 'when blocking target_account' do - it 'returns destroyed Block' do - account.block_relationships.create(target_account: target_account) - expect(subject).to be_a Block - expect(subject).to be_destroyed - end - end - - context 'when not blocking target_account' do - it 'returns nil' do - expect(subject).to be_nil - end - end - end - - describe '#unmute!' do - subject { account.unmute!(target_account) } - - context 'when muting target_account' do - it 'returns destroyed Mute' do - account.mute_relationships.create(target_account: target_account) - expect(subject).to be_a Mute - expect(subject).to be_destroyed - end - end - - context 'when not muting target_account' do - it 'returns nil' do - expect(subject).to be_nil - end - end - end - - describe '#unmute_conversation!' do - subject { account.unmute_conversation!(conversation) } - - let(:conversation) { Fabricate(:conversation) } - - context 'when muting the conversation' do - it 'returns destroyed ConversationMute' do - account.conversation_mutes.create(conversation: conversation) - expect(subject).to be_a ConversationMute - expect(subject).to be_destroyed - end - end - - context 'when not muting the conversation' do - it 'returns nil' do - expect(subject).to be_nil - end - end - end - - describe '#unblock_domain!' do - subject { account.unblock_domain!(domain) } - - let(:domain) { 'example.com' } - - context 'when blocking the domain' do - it 'returns destroyed AccountDomainBlock' do - account_domain_block = Fabricate(:account_domain_block, domain: domain) - account.domain_blocks << account_domain_block - expect(subject).to be_a AccountDomainBlock - expect(subject).to be_destroyed - end - end - - context 'when unblocking the domain' do - it 'returns nil' do - expect(subject).to be_nil - end - end - end - - describe '#unblock_idna_domain!' do - subject { account.unblock_domain!(punycode_domain) } - - let(:idna_domain) { '대한민국.한국' } - let(:punycode_domain) { 'xn--3e0bs9hfvinn1a.xn--3e0b707e' } - - context 'when blocking the domain' do - it 'returns destroyed AccountDomainBlock' do - account_domain_block = Fabricate(:account_domain_block, domain: idna_domain) - account.domain_blocks << account_domain_block - expect(subject).to be_a AccountDomainBlock - expect(subject).to be_destroyed - end - end - - context 'when unblocking idna domain' do - it 'returns nil' do - expect(subject).to be_nil - end - end - end - - describe '#following?' do - subject { account.following?(target_account) } - - context 'when following target_account' do - it 'returns true' do - account.active_relationships.create(target_account: target_account) - expect(subject).to be true - end - end - - context 'when not following target_account' do - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '#followed_by?' do - subject { account.followed_by?(target_account) } - - context 'when followed by target_account' do - it 'returns true' do - account.passive_relationships.create(account: target_account) - expect(subject).to be true - end - end - - context 'when not followed by target_account' do - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '#blocking?' do - subject { account.blocking?(target_account) } - - context 'when blocking target_account' do - it 'returns true' do - account.block_relationships.create(target_account: target_account) - expect(subject).to be true - end - end - - context 'when not blocking target_account' do - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '#domain_blocking?' do - subject { account.domain_blocking?(domain) } - - let(:domain) { 'example.com' } - - context 'when blocking the domain' do - it 'returns true' do - account_domain_block = Fabricate(:account_domain_block, domain: domain) - account.domain_blocks << account_domain_block - expect(subject).to be true - end - end - - context 'when not blocking the domain' do - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '#muting?' do - subject { account.muting?(target_account) } - - context 'when muting target_account' do - it 'returns true' do - mute = Fabricate(:mute, account: account, target_account: target_account) - account.mute_relationships << mute - expect(subject).to be true - end - end - - context 'when not muting target_account' do - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '#muting_conversation?' do - subject { account.muting_conversation?(conversation) } - - let(:conversation) { Fabricate(:conversation) } - - context 'when muting the conversation' do - it 'returns true' do - account.conversation_mutes.create(conversation: conversation) - expect(subject).to be true - end - end - - context 'when not muting the conversation' do - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '#muting_notifications?' do - subject { account.muting_notifications?(target_account) } - - before do - mute = Fabricate(:mute, target_account: target_account, account: account, hide_notifications: hide) - account.mute_relationships << mute - end - - context 'when muting notifications of target_account' do - let(:hide) { true } - - it 'returns true' do - expect(subject).to be true - end - end - - context 'when not muting notifications of target_account' do - let(:hide) { false } - - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '#requested?' do - subject { account.requested?(target_account) } - - context 'with requested by target_account' do - it 'returns true' do - Fabricate(:follow_request, account: account, target_account: target_account) - expect(subject).to be true - end - end - - context 'when not requested by target_account' do - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '#favourited?' do - subject { account.favourited?(status) } - - let(:status) { Fabricate(:status, account: account, favourites: favourites) } - - context 'when favorited' do - let(:favourites) { [Fabricate(:favourite, account: account)] } - - it 'returns true' do - expect(subject).to be true - end - end - - context 'when not favorited' do - let(:favourites) { [] } - - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '#reblogged?' do - subject { account.reblogged?(status) } - - let(:status) { Fabricate(:status, account: account, reblogs: reblogs) } - - context 'with reblogged' do - let(:reblogs) { [Fabricate(:status, account: account)] } - - it 'returns true' do - expect(subject).to be true - end - end - - context 'when not reblogged' do - let(:reblogs) { [] } - - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '#pinned?' do - subject { account.pinned?(status) } - - let(:status) { Fabricate(:status, account: account) } - - context 'when pinned' do - it 'returns true' do - Fabricate(:status_pin, account: account, status: status) - expect(subject).to be true - end - end - - context 'when not pinned' do - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '#remote_followers_hash' do - let(:me) { Fabricate(:account, username: 'Me') } - let(:remote_alice) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') } - let(:remote_bob) { Fabricate(:account, username: 'bob', domain: 'example.org', uri: 'https://example.org/users/bob') } - let(:remote_instance_actor) { Fabricate(:account, username: 'instance-actor', domain: 'example.org', uri: 'https://example.org') } - let(:remote_eve) { Fabricate(:account, username: 'eve', domain: 'foo.org', uri: 'https://foo.org/users/eve') } - - before do - remote_alice.follow!(me) - remote_bob.follow!(me) - remote_instance_actor.follow!(me) - remote_eve.follow!(me) - me.follow!(remote_alice) - end - - it 'returns correct hash for remote domains' do - expect(me.remote_followers_hash('https://example.org/')).to eq '20aecbe774b3d61c25094370baf370012b9271c5b172ecedb05caff8d79ef0c7' - expect(me.remote_followers_hash('https://foo.org/')).to eq 'ccb9c18a67134cfff9d62c7f7e7eb88e6b803446c244b84265565f4eba29df0e' - expect(me.remote_followers_hash('https://foo.org.evil.com/')).to eq '0000000000000000000000000000000000000000000000000000000000000000' - expect(me.remote_followers_hash('https://foo')).to eq '0000000000000000000000000000000000000000000000000000000000000000' - end - - it 'invalidates cache as needed when removing or adding followers' do - expect(me.remote_followers_hash('https://example.org/')).to eq '20aecbe774b3d61c25094370baf370012b9271c5b172ecedb05caff8d79ef0c7' - remote_instance_actor.unfollow!(me) - expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec' - remote_alice.unfollow!(me) - expect(me.remote_followers_hash('https://example.org/')).to eq '241b00794ce9b46aa864f3220afadef128318da2659782985bac5ed5bd436bff' - remote_alice.follow!(me) - expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec' - end - end - - describe '#local_followers_hash' do - let(:me) { Fabricate(:account, username: 'Me') } - let(:remote_alice) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') } - - before do - me.follow!(remote_alice) - end - - it 'returns correct hash for local users' do - expect(remote_alice.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me)) - end - - it 'invalidates cache as needed when removing or adding followers' do - expect(remote_alice.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me)) - me.unfollow!(remote_alice) - expect(remote_alice.local_followers_hash).to eq '0000000000000000000000000000000000000000000000000000000000000000' - me.follow!(remote_alice) - expect(remote_alice.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me)) - end - end - - describe 'muting an account' do - let(:me) { Fabricate(:account, username: 'Me') } - let(:you) { Fabricate(:account, username: 'You') } - - context 'with the notifications option unspecified' do - before do - me.mute!(you) - end - - it 'defaults to muting notifications' do - expect(me.muting_notifications?(you)).to be true - end - end - - context 'with the notifications option set to false' do - before do - me.mute!(you, notifications: false) - end - - it 'does not mute notifications' do - expect(me.muting_notifications?(you)).to be false - end - end - - context 'with the notifications option set to true' do - before do - me.mute!(you, notifications: true) - end - - it 'does mute notifications' do - expect(me.muting_notifications?(you)).to be true - end - end - end - - describe 'ignoring reblogs from an account' do - let!(:me) { Fabricate(:account, username: 'Me') } - let!(:you) { Fabricate(:account, username: 'You') } - - context 'with the reblogs option unspecified' do - before do - me.follow!(you) - end - - it 'defaults to showing reblogs' do - expect(me.muting_reblogs?(you)).to be(false) - end - end - - context 'with the reblogs option set to false' do - before do - me.follow!(you, reblogs: false) - end - - it 'does mute reblogs' do - expect(me.muting_reblogs?(you)).to be(true) - end - end - - context 'with the reblogs option set to true' do - before do - me.follow!(you, reblogs: true) - end - - it 'does not mute reblogs' do - expect(me.muting_reblogs?(you)).to be(false) - end - end - end - - describe '#lists_for_local_distribution' do - let(:account) { Fabricate(:user, current_sign_in_at: Time.now.utc).account } - let!(:inactive_follower_user) { Fabricate(:user, current_sign_in_at: 5.years.ago) } - let!(:follower_user) { Fabricate(:user, current_sign_in_at: Time.now.utc) } - let!(:follow_request_user) { Fabricate(:user, current_sign_in_at: Time.now.utc) } - - let!(:inactive_follower_list) { Fabricate(:list, account: inactive_follower_user.account) } - let!(:follower_list) { Fabricate(:list, account: follower_user.account) } - let!(:follow_request_list) { Fabricate(:list, account: follow_request_user.account) } - - let!(:self_list) { Fabricate(:list, account: account) } - - before do - inactive_follower_user.account.follow!(account) - follower_user.account.follow!(account) - follow_request_user.account.follow_requests.create!(target_account: account) - - inactive_follower_list.accounts << account - follower_list.accounts << account - follow_request_list.accounts << account - self_list.accounts << account - end - - it 'includes only the list from the active follower and from oneself' do - expect(account.lists_for_local_distribution.to_a).to contain_exactly(follower_list, self_list) - end - end -end diff --git a/spec/models/concerns/account/statuses_search_spec.rb b/spec/models/concerns/account/statuses_search_spec.rb deleted file mode 100644 index ab249d62d0..0000000000 --- a/spec/models/concerns/account/statuses_search_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Account::StatusesSearch do - let(:account) { Fabricate(:account, indexable: indexable) } - - before do - allow(Chewy).to receive(:enabled?).and_return(true) - end - - describe '#enqueue_update_public_statuses_index' do - before do - allow(account).to receive(:enqueue_add_to_public_statuses_index) - allow(account).to receive(:enqueue_remove_from_public_statuses_index) - end - - context 'when account is indexable' do - let(:indexable) { true } - - it 'enqueues add_to_public_statuses_index and not to remove_from_public_statuses_index' do - account.enqueue_update_public_statuses_index - expect(account).to have_received(:enqueue_add_to_public_statuses_index) - expect(account).to_not have_received(:enqueue_remove_from_public_statuses_index) - end - end - - context 'when account is not indexable' do - let(:indexable) { false } - - it 'enqueues remove_from_public_statuses_index and not to add_to_public_statuses_index' do - account.enqueue_update_public_statuses_index - expect(account).to have_received(:enqueue_remove_from_public_statuses_index) - expect(account).to_not have_received(:enqueue_add_to_public_statuses_index) - end - end - end - - describe '#enqueue_add_to_public_statuses_index' do - let(:indexable) { true } - let(:worker) { AddToPublicStatusesIndexWorker } - - before do - allow(worker).to receive(:perform_async) - end - - it 'enqueues AddToPublicStatusesIndexWorker' do - account.enqueue_add_to_public_statuses_index - expect(worker).to have_received(:perform_async).with(account.id) - end - end - - describe '#enqueue_remove_from_public_statuses_index' do - let(:indexable) { false } - let(:worker) { RemoveFromPublicStatusesIndexWorker } - - before do - allow(worker).to receive(:perform_async) - end - - it 'enqueues RemoveFromPublicStatusesIndexWorker' do - account.enqueue_remove_from_public_statuses_index - expect(worker).to have_received(:perform_async).with(account.id) - end - end -end diff --git a/spec/models/concerns/remotable_spec.rb b/spec/models/concerns/remotable_spec.rb deleted file mode 100644 index 097e6bf006..0000000000 --- a/spec/models/concerns/remotable_spec.rb +++ /dev/null @@ -1,219 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Remotable do - let(:foo_class) do - Class.new do - def initialize - @attrs = {} - end - - def [](arg) - @attrs[arg] - end - - def []=(arg1, arg2) - @attrs[arg1] = arg2 - end - - def hoge=(arg); end - - def hoge_file_name; end - - def hoge_file_name=(arg); end - - def has_attribute?(arg); end - - def self.attachment_definitions - { hoge: nil } - end - end - end - - let(:attribute_name) { :"#{hoge}_remote_url" } - let(:code) { 200 } - let(:file) { 'filename="foo.txt"' } - let(:foo) { foo_class.new } - let(:headers) { { 'content-disposition' => file } } - let(:hoge) { :hoge } - let(:url) { 'https://google.com' } - - before do - foo_class.include described_class - foo_class.remotable_attachment :hoge, 1.kilobyte - end - - it 'defines a method #hoge_remote_url=' do - expect(foo).to respond_to(:hoge_remote_url=) - end - - it 'defines a method #reset_hoge!' do - expect(foo).to respond_to(:reset_hoge!) - end - - it 'defines a method #download_hoge!' do - expect(foo).to respond_to(:download_hoge!) - end - - describe '#hoge_remote_url=' do - before do - stub_request(:get, url).to_return(status: code, headers: headers) - end - - it 'always returns its argument' do - [nil, '', [], {}].each do |arg| - expect(foo.hoge_remote_url = arg).to be arg - end - end - - context 'with an invalid URL' do - before do - parsed = instance_double(Addressable::URI) - allow(parsed).to receive(:normalize).with(no_args).and_raise(Addressable::URI::InvalidURIError) - allow(Addressable::URI).to receive(:parse).with(url).and_return(parsed) - end - - it 'makes no request' do - foo.hoge_remote_url = url - expect(a_request(:get, url)).to_not have_been_made - end - end - - context 'with scheme that is neither http nor https' do - let(:url) { 'ftp://google.com' } - - it 'makes no request' do - foo.hoge_remote_url = url - expect(a_request(:get, url)).to_not have_been_made - end - end - - context 'with relative URL' do - let(:url) { 'https:///path' } - - it 'makes no request' do - foo.hoge_remote_url = url - expect(a_request(:get, url)).to_not have_been_made - end - end - - context 'when URL has not changed' do - it 'makes no request if file is already saved' do - allow(foo).to receive(:[]).with(attribute_name).and_return(url) - allow(foo).to receive(:hoge_file_name).and_return('foo.jpg') - - foo.hoge_remote_url = url - expect(a_request(:get, url)).to_not have_been_made - end - - it 'makes request if file is not already saved' do - allow(foo).to receive(:[]).with(attribute_name).and_return(url) - allow(foo).to receive(:hoge_file_name).and_return(nil) - - foo.hoge_remote_url = url - expect(a_request(:get, url)).to have_been_made - end - end - - context 'when instance has no attribute for URL' do - before do - allow(foo).to receive(:has_attribute?).with(attribute_name).and_return(false) - end - - it 'does not try to write attribute' do - allow(foo).to receive('[]=').with(attribute_name, url) - - foo.hoge_remote_url = url - - expect(foo).to_not have_received('[]=').with(attribute_name, url) - end - end - - context 'when instance has an attribute for URL' do - before do - allow(foo).to receive(:has_attribute?).with(attribute_name).and_return(true) - end - - it 'does not try to write attribute' do - allow(foo).to receive('[]=').with(attribute_name, url) - - foo.hoge_remote_url = url - - expect(foo).to have_received('[]=').with(attribute_name, url) - end - end - - context 'with a valid URL' do - it 'makes a request' do - foo.hoge_remote_url = url - expect(a_request(:get, url)).to have_been_made - end - - context 'when the response is not successful' do - let(:code) { 500 } - - it 'does not assign file' do - allow(foo).to receive(:public_send) - allow(foo).to receive(:public_send) - - foo.hoge_remote_url = url - - expect(foo).to_not have_received(:public_send).with("#{hoge}=", any_args) - expect(foo).to_not have_received(:public_send).with("#{hoge}_file_name=", any_args) - end - end - - context 'when the response is successful' do - let(:code) { 200 } - - context 'when contains Content-Disposition header' do - let(:file) { 'filename="foo.txt"' } - let(:headers) { { 'content-disposition' => file } } - - it 'assigns file' do - response_with_limit = ResponseWithLimit.new(nil, 0) - - allow(ResponseWithLimit).to receive(:new).with(anything, anything).and_return(response_with_limit) - - allow(foo).to receive(:public_send) - foo.hoge_remote_url = url - expect(foo).to have_received(:public_send).with(:"download_#{hoge}!", url) - - allow(foo).to receive(:public_send) - foo.download_hoge!(url) - expect(foo).to have_received(:public_send).with(:"#{hoge}=", response_with_limit) - end - end - end - - context 'when an error is raised during the request' do - before do - stub_request(:get, url).to_raise(error_class) - end - - error_classes = [ - HTTP::TimeoutError, - HTTP::ConnectionError, - OpenSSL::SSL::SSLError, - Paperclip::Errors::NotIdentifiedByImageMagickError, - Addressable::URI::InvalidURIError, - ] - - error_classes.each do |error_class| - let(:error_class) { error_class } - - it 'calls Rails.logger.debug' do - allow(Rails.logger).to receive(:debug) - - foo.hoge_remote_url = url - - expect(Rails.logger).to have_received(:debug) do |&block| - expect(block.call).to match(/^Error fetching remote #{hoge}: /) - end - end - end - end - end - end -end diff --git a/spec/models/concerns/status/threading_concern_spec.rb b/spec/models/concerns/status/threading_concern_spec.rb deleted file mode 100644 index 09fb218566..0000000000 --- a/spec/models/concerns/status/threading_concern_spec.rb +++ /dev/null @@ -1,132 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Status::ThreadingConcern do - describe '#ancestors' do - let!(:alice) { Fabricate(:account, username: 'alice') } - let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } - let!(:jeff) { Fabricate(:account, username: 'jeff') } - let!(:status) { Fabricate(:status, account: alice) } - let!(:reply_to_status) { Fabricate(:status, thread: status, account: jeff) } - let!(:reply_to_first_reply) { Fabricate(:status, thread: reply_to_status, account: bob) } - let!(:reply_to_second_reply) { Fabricate(:status, thread: reply_to_first_reply, account: alice) } - let!(:viewer) { Fabricate(:account, username: 'viewer') } - - it 'returns conversation history' do - expect(reply_to_second_reply.ancestors(4)).to include(status, reply_to_status, reply_to_first_reply) - end - - it 'does not return conversation history user is not allowed to see' do - reply_to_status.update(visibility: :private) - status.update(visibility: :direct) - - expect(reply_to_second_reply.ancestors(4, viewer)).to_not include(reply_to_status, status) - end - - it 'does not return conversation history from blocked users' do - viewer.block!(jeff) - expect(reply_to_second_reply.ancestors(4, viewer)).to_not include(reply_to_status) - end - - it 'does not return conversation history from muted users' do - viewer.mute!(jeff) - expect(reply_to_second_reply.ancestors(4, viewer)).to_not include(reply_to_status) - end - - it 'does not return conversation history from silenced and not followed users' do - jeff.silence! - expect(reply_to_second_reply.ancestors(4, viewer)).to_not include(reply_to_status) - end - - it 'does not return conversation history from blocked domains' do - viewer.block_domain!('example.com') - expect(reply_to_second_reply.ancestors(4, viewer)).to_not include(reply_to_first_reply) - end - - it 'ignores deleted records' do - first_status = Fabricate(:status, account: bob) - second_status = Fabricate(:status, thread: first_status, account: alice) - - # Create cache and delete cached record - second_status.ancestors(4) - first_status.destroy - - expect(second_status.ancestors(4)).to eq([]) - end - - it 'can return more records than previously requested' do - first_status = Fabricate(:status, account: bob) - second_status = Fabricate(:status, thread: first_status, account: alice) - third_status = Fabricate(:status, thread: second_status, account: alice) - - # Create cache - second_status.ancestors(1) - - expect(third_status.ancestors(2)).to eq([first_status, second_status]) - end - - it 'can return fewer records than previously requested' do - first_status = Fabricate(:status, account: bob) - second_status = Fabricate(:status, thread: first_status, account: alice) - third_status = Fabricate(:status, thread: second_status, account: alice) - - # Create cache - second_status.ancestors(2) - - expect(third_status.ancestors(1)).to eq([second_status]) - end - end - - describe '#descendants' do - let!(:alice) { Fabricate(:account, username: 'alice') } - let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } - let!(:jeff) { Fabricate(:account, username: 'jeff') } - let!(:status) { Fabricate(:status, account: alice) } - let!(:reply_to_status_from_alice) { Fabricate(:status, thread: status, account: alice) } - let!(:reply_to_status_from_bob) { Fabricate(:status, thread: status, account: bob) } - let!(:reply_to_alice_reply_from_jeff) { Fabricate(:status, thread: reply_to_status_from_alice, account: jeff) } - let!(:viewer) { Fabricate(:account, username: 'viewer') } - - it 'returns replies' do - expect(status.descendants(4)).to include(reply_to_status_from_alice, reply_to_status_from_bob, reply_to_alice_reply_from_jeff) - end - - it 'does not return replies user is not allowed to see' do - reply_to_status_from_alice.update(visibility: :private) - reply_to_alice_reply_from_jeff.update(visibility: :direct) - - expect(status.descendants(4, viewer)).to_not include(reply_to_status_from_alice, reply_to_alice_reply_from_jeff) - end - - it 'does not return replies from blocked users' do - viewer.block!(jeff) - expect(status.descendants(4, viewer)).to_not include(reply_to_alice_reply_from_jeff) - end - - it 'does not return replies from muted users' do - viewer.mute!(jeff) - expect(status.descendants(4, viewer)).to_not include(reply_to_alice_reply_from_jeff) - end - - it 'does not return replies from silenced and not followed users' do - jeff.silence! - expect(status.descendants(4, viewer)).to_not include(reply_to_alice_reply_from_jeff) - end - - it 'does not return replies from blocked domains' do - viewer.block_domain!('example.com') - expect(status.descendants(4, viewer)).to_not include(reply_to_status_from_bob) - end - - it 'promotes self-replies to the top while leaving the rest in order' do - a = Fabricate(:status, account: alice) - d = Fabricate(:status, account: jeff, thread: a) - e = Fabricate(:status, account: bob, thread: d) - c = Fabricate(:status, account: alice, thread: a) - f = Fabricate(:status, account: bob, thread: c) - - expect(a.descendants(20)).to eq [c, d, e, f] - end - end -end diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb deleted file mode 100644 index c1d6659aa7..0000000000 --- a/spec/models/conversation_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Conversation do - describe '#local?' do - it 'returns true when URI is nil' do - expect(Fabricate(:conversation).local?).to be true - end - - it 'returns false when URI is not nil' do - expect(Fabricate(:conversation, uri: 'abc').local?).to be false - end - end -end diff --git a/spec/models/custom_emoji_category_spec.rb b/spec/models/custom_emoji_category_spec.rb deleted file mode 100644 index 30de07bd81..0000000000 --- a/spec/models/custom_emoji_category_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -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/custom_emoji_filter_spec.rb b/spec/models/custom_emoji_filter_spec.rb deleted file mode 100644 index c36fecd60d..0000000000 --- a/spec/models/custom_emoji_filter_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe CustomEmojiFilter do - describe '#results' do - subject { described_class.new(params).results } - - let!(:custom_emoji_domain_a) { Fabricate(:custom_emoji, domain: 'a') } - let!(:custom_emoji_domain_b) { Fabricate(:custom_emoji, domain: 'b') } - let!(:custom_emoji_domain_nil) { Fabricate(:custom_emoji, domain: nil, shortcode: 'hoge') } - - context 'when params have values' do - context 'when local' do - let(:params) { { local: true } } - - it 'returns ActiveRecord::Relation' do - expect(subject).to be_a(ActiveRecord::Relation) - expect(subject).to contain_exactly(custom_emoji_domain_nil) - end - end - - context 'when remote' do - let(:params) { { remote: true } } - - it 'returns ActiveRecord::Relation' do - expect(subject).to be_a(ActiveRecord::Relation) - expect(subject).to contain_exactly(custom_emoji_domain_a, custom_emoji_domain_b) - end - end - - context 'with by_domain' do - let(:params) { { by_domain: 'a' } } - - it 'returns ActiveRecord::Relation' do - expect(subject).to be_a(ActiveRecord::Relation) - expect(subject).to contain_exactly(custom_emoji_domain_a) - end - end - - context 'when shortcode' do - let(:params) { { shortcode: 'hoge' } } - - it 'returns ActiveRecord::Relation' do - expect(subject).to be_a(ActiveRecord::Relation) - expect(subject).to contain_exactly(custom_emoji_domain_nil) - end - end - - context 'when some other case' do - let(:params) { { else: 'else' } } - - it 'raises Mastodon::InvalidParameterError' do - expect do - subject - end.to raise_error(Mastodon::InvalidParameterError, /Unknown filter: else/) - end - end - end - - context 'when params without value' do - let(:params) { { hoge: nil } } - - it 'returns ActiveRecord::Relation' do - expect(subject).to be_a(ActiveRecord::Relation) - expect(subject).to contain_exactly(custom_emoji_domain_a, custom_emoji_domain_b, custom_emoji_domain_nil) - end - end - end -end diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb deleted file mode 100644 index cb8cb5c11b..0000000000 --- a/spec/models/custom_emoji_spec.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe CustomEmoji, :attachment_processing do - describe '#search' do - subject { described_class.search(search_term) } - - let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: shortcode) } - - context 'when shortcode is exact' do - let(:shortcode) { 'blobpats' } - let(:search_term) { 'blobpats' } - - it 'finds emoji' do - expect(subject).to include(custom_emoji) - end - end - - context 'when shortcode is partial' do - let(:shortcode) { 'blobpats' } - let(:search_term) { 'blob' } - - it 'finds emoji' do - expect(subject).to include(custom_emoji) - end - end - end - - describe '#local?' do - subject { custom_emoji.local? } - - let(:custom_emoji) { Fabricate(:custom_emoji, domain: domain) } - - context 'when domain is nil' do - let(:domain) { nil } - - it 'returns true' do - expect(subject).to be true - end - end - - context 'when domain is present' do - let(:domain) { 'example.com' } - - it 'returns false' do - expect(subject).to be false - end - end - end - - describe '#object_type' do - it 'returns :emoji' do - custom_emoji = Fabricate(:custom_emoji) - expect(custom_emoji.object_type).to be :emoji - end - end - - describe '.from_text' do - subject { described_class.from_text(text, nil) } - - let!(:emojo) { Fabricate(:custom_emoji, shortcode: 'coolcat') } - - context 'with plain text' do - let(:text) { 'Hello :coolcat:' } - - it 'returns records used via shortcodes in text' do - expect(subject).to include(emojo) - end - end - - context 'with html' do - let(:text) { '

Hello :coolcat:

' } - - it 'returns records used via shortcodes in text' do - expect(subject).to include(emojo) - end - end - end - - describe 'Normalizations' do - describe 'downcase domain value' do - context 'with a mixed case domain value' do - it 'normalizes the value to downcased' do - custom_emoji = Fabricate.build(:custom_emoji, domain: 'wWw.MaStOdOn.CoM') - - expect(custom_emoji.domain).to eq('www.mastodon.com') - end - end - - context 'with a nil domain value' do - it 'leaves the value as nil' do - custom_emoji = Fabricate.build(:custom_emoji, domain: nil) - - expect(custom_emoji.domain).to be_nil - end - end - end - end -end diff --git a/spec/models/custom_filter_keyword_spec.rb b/spec/models/custom_filter_keyword_spec.rb deleted file mode 100644 index 4e3ab060a0..0000000000 --- a/spec/models/custom_filter_keyword_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe CustomFilterKeyword do - describe '#to_regex' do - context 'when whole_word is true' do - it 'builds a regex with boundaries and the keyword' do - keyword = described_class.new(whole_word: true, keyword: 'test') - - expect(keyword.to_regex).to eq(/(?mix:\b#{Regexp.escape(keyword.keyword)}\b)/) - end - - it 'builds a regex with starting boundary and the keyword when end with non-word' do - keyword = described_class.new(whole_word: true, keyword: 'test#') - - expect(keyword.to_regex).to eq(/(?mix:\btest\#)/) - end - - it 'builds a regex with end boundary and the keyword when start with non-word' do - keyword = described_class.new(whole_word: true, keyword: '#test') - - expect(keyword.to_regex).to eq(/(?mix:\#test\b)/) - end - end - - context 'when whole_word is false' do - it 'builds a regex with the keyword' do - keyword = described_class.new(whole_word: false, keyword: 'test') - - expect(keyword.to_regex).to eq(/test/i) - end - end - end -end diff --git a/spec/models/custom_filter_spec.rb b/spec/models/custom_filter_spec.rb deleted file mode 100644 index 8ac9dbb896..0000000000 --- a/spec/models/custom_filter_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe CustomFilter do - describe 'Validations' do - it 'requires presence of title' do - record = described_class.new(title: '') - record.valid? - - expect(record).to model_have_error_on_field(:title) - end - - it 'requires presence of context' do - record = described_class.new(context: nil) - record.valid? - - expect(record).to model_have_error_on_field(:context) - end - - it 'requires non-empty of context' do - record = described_class.new(context: []) - record.valid? - - expect(record).to model_have_error_on_field(:context) - end - - it 'requires valid context value' do - record = described_class.new(context: ['invalid']) - record.valid? - - expect(record).to model_have_error_on_field(:context) - end - end - - describe 'Normalizations' do - it 'cleans up context values' do - record = described_class.new(context: ['home', 'notifications', 'public ', '']) - - expect(record.context).to eq(%w(home notifications public)) - end - end -end diff --git a/spec/models/domain_allow_spec.rb b/spec/models/domain_allow_spec.rb deleted file mode 100644 index 12504211a1..0000000000 --- a/spec/models/domain_allow_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe DomainAllow do - describe 'Validations' do - it 'is invalid without a domain' do - domain_allow = Fabricate.build(:domain_allow, domain: nil) - domain_allow.valid? - expect(domain_allow).to model_have_error_on_field(:domain) - end - - it 'is invalid if the same normalized domain already exists' do - _domain_allow = Fabricate(:domain_allow, domain: 'にゃん') - domain_allow_with_normalized_value = Fabricate.build(:domain_allow, domain: 'xn--r9j5b5b') - domain_allow_with_normalized_value.valid? - expect(domain_allow_with_normalized_value).to model_have_error_on_field(:domain) - end - end -end diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb deleted file mode 100644 index d595441fd3..0000000000 --- a/spec/models/domain_block_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe DomainBlock do - describe 'validations' do - it 'is invalid without a domain' do - domain_block = Fabricate.build(:domain_block, domain: nil) - domain_block.valid? - expect(domain_block).to model_have_error_on_field(:domain) - end - - it 'is invalid if the same normalized domain already exists' do - _domain_block = Fabricate(:domain_block, domain: 'にゃん') - domain_block_with_normalized_value = Fabricate.build(:domain_block, domain: 'xn--r9j5b5b') - domain_block_with_normalized_value.valid? - expect(domain_block_with_normalized_value).to model_have_error_on_field(:domain) - end - end - - describe '.blocked?' do - it 'returns true if the domain is suspended' do - Fabricate(:domain_block, domain: 'example.com', severity: :suspend) - expect(described_class.blocked?('example.com')).to be true - end - - it 'returns false even if the domain is silenced' do - Fabricate(:domain_block, domain: 'example.com', severity: :silence) - expect(described_class.blocked?('example.com')).to be false - end - - it 'returns false if the domain is not suspended nor silenced' do - expect(described_class.blocked?('example.com')).to be false - end - end - - describe '.rule_for' do - it 'returns rule matching a blocked domain' do - block = Fabricate(:domain_block, domain: 'example.com') - expect(described_class.rule_for('example.com')).to eq block - end - - it 'returns a rule matching a subdomain of a blocked domain' do - block = Fabricate(:domain_block, domain: 'example.com') - expect(described_class.rule_for('sub.example.com')).to eq block - end - - it 'returns a rule matching a blocked subdomain' do - block = Fabricate(:domain_block, domain: 'sub.example.com') - expect(described_class.rule_for('sub.example.com')).to eq block - end - - it 'returns a rule matching a blocked TLD' do - block = Fabricate(:domain_block, domain: 'google') - expect(described_class.rule_for('google')).to eq block - end - - it 'returns a rule matching a subdomain of a blocked TLD' do - block = Fabricate(:domain_block, domain: 'google') - expect(described_class.rule_for('maps.google')).to eq block - end - end - - describe '#stricter_than?' do - it 'returns true if the new block has suspend severity while the old has lower severity' do - suspend = described_class.new(domain: 'domain', severity: :suspend) - silence = described_class.new(domain: 'domain', severity: :silence) - noop = described_class.new(domain: 'domain', severity: :noop) - expect(suspend.stricter_than?(silence)).to be true - expect(suspend.stricter_than?(noop)).to be true - end - - it 'returns false if the new block has lower severity than the old one' do - suspend = described_class.new(domain: 'domain', severity: :suspend) - silence = described_class.new(domain: 'domain', severity: :silence) - noop = described_class.new(domain: 'domain', severity: :noop) - expect(silence.stricter_than?(suspend)).to be false - expect(noop.stricter_than?(suspend)).to be false - expect(noop.stricter_than?(silence)).to be false - end - - it 'returns false if the new block does is less strict regarding reports' do - older = described_class.new(domain: 'domain', severity: :silence, reject_reports: true) - newer = described_class.new(domain: 'domain', severity: :silence, reject_reports: false) - expect(newer.stricter_than?(older)).to be false - end - - it 'returns false if the new block does is less strict regarding media' do - older = described_class.new(domain: 'domain', severity: :silence, reject_media: true) - newer = described_class.new(domain: 'domain', severity: :silence, reject_media: false) - expect(newer.stricter_than?(older)).to be false - end - end - - describe '#public_domain' do - context 'with a domain block that is obfuscated' do - let(:domain_block) { Fabricate(:domain_block, domain: 'hostname.example.com', obfuscate: true) } - - it 'garbles the domain' do - expect(domain_block.public_domain).to eq 'hostna**.******e.com' - end - end - - context 'with a domain block that is not obfuscated' do - let(:domain_block) { Fabricate(:domain_block, domain: 'example.com', obfuscate: false) } - - it 'returns the domain value' do - expect(domain_block.public_domain).to eq 'example.com' - end - end - end -end diff --git a/spec/models/email_domain_block_spec.rb b/spec/models/email_domain_block_spec.rb deleted file mode 100644 index 5874c5e53c..0000000000 --- a/spec/models/email_domain_block_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe EmailDomainBlock do - describe 'block?' do - let(:input) { nil } - - context 'when given an e-mail address' do - let(:input) { "foo@#{domain}" } - - context 'with a top level domain' do - let(:domain) { 'example.com' } - - it 'returns true if the domain is blocked' do - Fabricate(:email_domain_block, domain: 'example.com') - expect(described_class.block?(input)).to be true - end - - it 'returns false if the domain is not blocked' do - Fabricate(:email_domain_block, domain: 'other-example.com') - expect(described_class.block?(input)).to be false - end - end - - context 'with a subdomain' do - let(:domain) { 'mail.example.com' } - - it 'returns true if it is a subdomain of a blocked domain' do - Fabricate(:email_domain_block, domain: 'example.com') - expect(described_class.block?(input)).to be true - end - end - end - - context 'when given an array of domains' do - let(:input) { %w(foo.com mail.foo.com) } - - it 'returns true if the domain is blocked' do - Fabricate(:email_domain_block, domain: 'mail.foo.com') - expect(described_class.block?(input)).to be true - end - end - end -end diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb deleted file mode 100644 index 75468898d2..0000000000 --- a/spec/models/export_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Export do - let(:account) { Fabricate(:account) } - let(:target_accounts) do - [{}, { username: 'one', domain: 'local.host' }].map(&method(:Fabricate).curry(2).call(:account)) - end - - describe 'to_csv' do - it 'returns a csv of the blocked accounts' do - target_accounts.each { |target_account| account.block!(target_account) } - - export = described_class.new(account).to_blocked_accounts_csv - results = export.strip.split - - expect(results.size).to eq 2 - expect(results.first).to eq 'one@local.host' - end - - it 'returns a csv of the muted accounts' do - target_accounts.each { |target_account| account.mute!(target_account) } - - export = described_class.new(account).to_muted_accounts_csv - results = export.strip.split("\n") - - expect(results.size).to eq 3 - expect(results.first).to eq 'Account address,Hide notifications' - expect(results.second).to eq 'one@local.host,true' - end - - it 'returns a csv of the following accounts' do - target_accounts.each { |target_account| account.follow!(target_account) } - - export = described_class.new(account).to_following_accounts_csv - results = export.strip.split("\n") - - expect(results.size).to eq 3 - expect(results.first).to eq 'Account address,Show boosts,Notify on new posts,Languages' - expect(results.second).to eq 'one@local.host,true,false,' - end - end - - describe 'total_storage' do - it 'returns the total size of the media attachments' do - media_attachment = Fabricate(:media_attachment, account: account) - expect(described_class.new(account).total_storage).to eq media_attachment.file_file_size || 0 - end - end - - describe 'total_follows' do - it 'returns the total number of the followed accounts' do - target_accounts.each { |target_account| account.follow!(target_account) } - expect(described_class.new(account.reload).total_follows).to eq 2 - end - - it 'returns the total number of the blocked accounts' do - target_accounts.each { |target_account| account.block!(target_account) } - expect(described_class.new(account.reload).total_blocks).to eq 2 - end - - it 'returns the total number of the muted accounts' do - target_accounts.each { |target_account| account.mute!(target_account) } - expect(described_class.new(account.reload).total_mutes).to eq 2 - end - end -end diff --git a/spec/models/extended_description_spec.rb b/spec/models/extended_description_spec.rb deleted file mode 100644 index ecc27c0f6d..0000000000 --- a/spec/models/extended_description_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -# 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/favourite_spec.rb b/spec/models/favourite_spec.rb deleted file mode 100644 index ef7fbdefcd..0000000000 --- a/spec/models/favourite_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Favourite do - let(:account) { Fabricate(:account) } - - context 'when status is a reblog' do - let(:reblog) { Fabricate(:status, reblog: nil) } - let(:status) { Fabricate(:status, reblog: reblog) } - - it 'invalidates if the reblogged status is already a favourite' do - described_class.create!(account: account, status: reblog) - expect(described_class.new(account: account, status: status).valid?).to be false - end - - it 'replaces status with the reblogged one if it is a reblog' do - favourite = described_class.create!(account: account, status: status) - expect(favourite.status).to eq reblog - end - end - - context 'when status is not a reblog' do - let(:status) { Fabricate(:status, reblog: nil) } - - it 'saves with the specified status' do - favourite = described_class.create!(account: account, status: status) - expect(favourite.status).to eq status - end - end -end diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb deleted file mode 100644 index f30e27e701..0000000000 --- a/spec/models/follow_request_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe FollowRequest do - describe '#authorize!' do - let!(:follow_request) { Fabricate(:follow_request, account: account, target_account: target_account) } - let(:account) { Fabricate(:account) } - let(:target_account) { Fabricate(:account) } - - context 'when the to-be-followed person has been added to a list' do - let!(:list) { Fabricate(:list, account: account) } - - before do - list.accounts << target_account - end - - it 'updates the ListAccount' do - expect { follow_request.authorize! }.to change { [list.list_accounts.first.follow_request_id, list.list_accounts.first.follow_id] }.from([follow_request.id, nil]).to([nil, anything]) - end - end - - it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do - allow(account).to receive(:follow!) do - account.active_relationships.create!(target_account: target_account) - end - allow(MergeWorker).to receive(:perform_async) - allow(follow_request).to receive(:destroy!) - - follow_request.authorize! - - expect(account).to have_received(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri, languages: nil, bypass_limit: true) - expect(MergeWorker).to have_received(:perform_async).with(target_account.id, account.id) - expect(follow_request).to have_received(:destroy!) - end - - it 'generates a Follow' do - follow_request = Fabricate.create(:follow_request) - follow_request.authorize! - target = follow_request.target_account - expect(follow_request.account.following?(target)).to be true - end - - it 'correctly passes show_reblogs when true' do - follow_request = Fabricate.create(:follow_request, show_reblogs: true) - follow_request.authorize! - target = follow_request.target_account - expect(follow_request.account.muting_reblogs?(target)).to be false - end - - it 'correctly passes show_reblogs when false' do - follow_request = Fabricate.create(:follow_request, show_reblogs: false) - follow_request.authorize! - target = follow_request.target_account - expect(follow_request.account.muting_reblogs?(target)).to be true - end - end - - describe '#reject!' do - let!(:follow_request) { Fabricate(:follow_request, account: account, target_account: target_account) } - let(:account) { Fabricate(:account) } - let(:target_account) { Fabricate(:account) } - - context 'when the to-be-followed person has been added to a list' do - let!(:list) { Fabricate(:list, account: account) } - - before do - list.accounts << target_account - end - - it 'deletes the ListAccount record' do - expect { follow_request.reject! }.to change { list.accounts.count }.from(1).to(0) - end - end - end -end diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb deleted file mode 100644 index 9aa172b2f2..0000000000 --- a/spec/models/follow_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Follow do - let(:alice) { Fabricate(:account, username: 'alice') } - let(:bob) { Fabricate(:account, username: 'bob') } - - describe 'validations' do - subject { described_class.new(account: alice, target_account: bob, rate_limit: true) } - - it 'is invalid without an account' do - follow = Fabricate.build(:follow, account: nil) - follow.valid? - expect(follow).to model_have_error_on_field(:account) - end - - it 'is invalid without a target_account' do - follow = Fabricate.build(:follow, target_account: nil) - follow.valid? - expect(follow).to model_have_error_on_field(:target_account) - end - - it 'is invalid if account already follows too many people' do - alice.update(following_count: FollowLimitValidator::LIMIT) - - expect(subject).to_not be_valid - expect(subject).to model_have_error_on_field(:base) - end - - it 'is valid if account is only on the brink of following too many people' do - alice.update(following_count: FollowLimitValidator::LIMIT - 1) - - expect(subject).to be_valid - expect(subject).to_not model_have_error_on_field(:base) - end - end - - describe '.recent' do - let!(:follow_earlier) { Fabricate(:follow) } - let!(:follow_later) { Fabricate(:follow) } - - it 'sorts with most recent follows first' do - results = described_class.recent - - expect(results.size).to eq 2 - expect(results).to eq [follow_later, follow_earlier] - end - end - - describe 'revoke_request!' do - let(:follow) { Fabricate(:follow, account: account, target_account: target_account) } - let(:account) { Fabricate(:account) } - let(:target_account) { Fabricate(:account) } - - it 'revokes the follow relation' do - follow.revoke_request! - expect(account.following?(target_account)).to be false - end - - it 'creates a follow request' do - follow.revoke_request! - expect(account.requested?(target_account)).to be true - end - end -end diff --git a/spec/models/form/account_batch_spec.rb b/spec/models/form/account_batch_spec.rb deleted file mode 100644 index 26fb1b953a..0000000000 --- a/spec/models/form/account_batch_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Form::AccountBatch do - let(:account_batch) { described_class.new } - - describe '#save' do - subject { account_batch.save } - - let(:account) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:account_ids) { [] } - let(:query) { Account.none } - - before do - account_batch.assign_attributes( - action: action, - current_account: account, - account_ids: account_ids, - query: query, - select_all_matching: select_all_matching - ) - end - - context 'when action is "suspend"' do - let(:action) { 'suspend' } - - let(:target_account) { Fabricate(:account) } - let(:target_account2) { Fabricate(:account) } - - before do - Fabricate(:report, target_account: target_account) - Fabricate(:report, target_account: target_account2) - end - - context 'when accounts are passed as account_ids' do - let(:select_all_matching) { '0' } - let(:account_ids) { [target_account.id, target_account2.id] } - - it 'suspends the expected users and closes open reports' do - expect { subject } - .to change_account_suspensions - .and change_open_reports_for_accounts - end - end - - context 'when accounts are passed as a query' do - let(:select_all_matching) { '1' } - let(:query) { Account.where(id: [target_account.id, target_account2.id]) } - - it 'suspends the expected users and closes open reports' do - expect { subject } - .to change_account_suspensions - .and change_open_reports_for_accounts - end - end - - private - - def change_account_suspensions - change { relevant_account_suspension_statuses } - .from([false, false]) - .to([true, true]) - end - - def change_open_reports_for_accounts - change(relevant_account_unresolved_reports, :count) - .from(2) - .to(0) - end - - def relevant_account_unresolved_reports - Report.unresolved.where(target_account: [target_account, target_account2]) - end - - def relevant_account_suspension_statuses - [target_account.reload, target_account2.reload].map(&:suspended?) - end - end - end -end diff --git a/spec/models/form/admin_settings_spec.rb b/spec/models/form/admin_settings_spec.rb deleted file mode 100644 index 0dc2d881ad..0000000000 --- a/spec/models/form/admin_settings_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Form::AdminSettings do - describe 'validations' do - describe 'site_contact_username' do - context 'with no accounts' do - it 'is not valid' do - setting = described_class.new(site_contact_username: 'Test') - setting.valid? - - expect(setting).to model_have_error_on_field(:site_contact_username) - end - end - - context 'with an account' do - before { Fabricate(:account, username: 'Glorp') } - - it 'is not valid when account doesnt match' do - setting = described_class.new(site_contact_username: 'Test') - setting.valid? - - expect(setting).to model_have_error_on_field(:site_contact_username) - end - - it 'is valid when account matches' do - setting = described_class.new(site_contact_username: 'Glorp') - setting.valid? - - expect(setting).to_not model_have_error_on_field(:site_contact_username) - end - end - end - end -end diff --git a/spec/models/form/custom_emoji_batch_spec.rb b/spec/models/form/custom_emoji_batch_spec.rb deleted file mode 100644 index abeada5d50..0000000000 --- a/spec/models/form/custom_emoji_batch_spec.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Form::CustomEmojiBatch do - describe '#save' do - subject { described_class.new({ current_account: account }.merge(options)) } - - let(:options) { {} } - let(:account) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - - context 'with empty custom_emoji_ids' do - let(:options) { { custom_emoji_ids: [] } } - - it 'does nothing if custom_emoji_ids is empty' do - expect(subject.save).to be_nil - end - end - - describe 'the update action' do - let(:custom_emoji) { Fabricate(:custom_emoji, category: Fabricate(:custom_emoji_category)) } - let(:custom_emoji_category) { Fabricate(:custom_emoji_category) } - - context 'without anything to change' do - let(:options) { { action: 'update' } } - - it 'silently exits without updating any custom emojis' do - expect { subject.save }.to_not change(Admin::ActionLog, :count) - end - end - - context 'with a category_id' do - let(:options) { { action: 'update', custom_emoji_ids: [custom_emoji.id], category_id: custom_emoji_category.id } } - - it 'updates the category of the emoji' do - subject.save - - expect(custom_emoji.reload.category).to eq(custom_emoji_category) - end - end - - context 'with a category_name' do - let(:options) { { action: 'update', custom_emoji_ids: [custom_emoji.id], category_name: custom_emoji_category.name } } - - it 'updates the category of the emoji' do - subject.save - - expect(custom_emoji.reload.category).to eq(custom_emoji_category) - end - end - end - - describe 'the list action' do - let(:custom_emoji) { Fabricate(:custom_emoji, visible_in_picker: false) } - let(:options) { { action: 'list', custom_emoji_ids: [custom_emoji.id] } } - - it 'updates the picker visibility of the emoji' do - subject.save - - expect(custom_emoji.reload.visible_in_picker).to be(true) - end - end - - describe 'the unlist action' do - let(:custom_emoji) { Fabricate(:custom_emoji, visible_in_picker: true) } - let(:options) { { action: 'unlist', custom_emoji_ids: [custom_emoji.id] } } - - it 'updates the picker visibility of the emoji' do - subject.save - - expect(custom_emoji.reload.visible_in_picker).to be(false) - end - end - - describe 'the enable action' do - let(:custom_emoji) { Fabricate(:custom_emoji, disabled: true) } - let(:options) { { action: 'enable', custom_emoji_ids: [custom_emoji.id] } } - - it 'updates the disabled value of the emoji' do - subject.save - - expect(custom_emoji.reload).to_not be_disabled - end - end - - describe 'the disable action' do - let(:custom_emoji) { Fabricate(:custom_emoji, visible_in_picker: false) } - let(:options) { { action: 'disable', custom_emoji_ids: [custom_emoji.id] } } - - it 'updates the disabled value of the emoji' do - subject.save - - expect(custom_emoji.reload).to be_disabled - end - end - - describe 'the copy action' do - let(:custom_emoji) { Fabricate(:custom_emoji) } - let(:options) { { action: 'copy', custom_emoji_ids: [custom_emoji.id] } } - - it 'makes a copy of the emoji' do - expect { subject.save } - .to change(CustomEmoji, :count).by(1) - end - end - - describe 'the delete action' do - let(:custom_emoji) { Fabricate(:custom_emoji) } - let(:options) { { action: 'delete', custom_emoji_ids: [custom_emoji.id] } } - - it 'destroys the emoji' do - subject.save - - expect { custom_emoji.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end -end diff --git a/spec/models/form/import_spec.rb b/spec/models/form/import_spec.rb deleted file mode 100644 index 22ffdfd877..0000000000 --- a/spec/models/form/import_spec.rb +++ /dev/null @@ -1,318 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Form::Import do - subject { described_class.new(current_account: account, type: import_type, mode: import_mode, data: data) } - - let(:account) { Fabricate(:account) } - let(:data) { fixture_file_upload(import_file) } - let(:import_mode) { 'merge' } - - describe 'validations' do - shared_examples 'incompatible import type' do |type, file| - let(:import_file) { file } - let(:import_type) { type } - - it 'has errors' do - subject.validate - expect(subject.errors[:data]).to include(I18n.t('imports.errors.incompatible_type')) - end - end - - shared_examples 'too many CSV rows' do |type, file, allowed_rows| - let(:import_file) { file } - let(:import_type) { type } - - before do - stub_const 'Form::Import::ROWS_PROCESSING_LIMIT', allowed_rows - end - - it 'has errors' do - subject.validate - expect(subject.errors[:data]).to include(I18n.t('imports.errors.over_rows_processing_limit', count: described_class::ROWS_PROCESSING_LIMIT)) - end - end - - shared_examples 'valid import' do |type, file| - let(:import_file) { file } - let(:import_type) { type } - - it 'passes validation' do - expect(subject).to be_valid - end - end - - context 'when the file too large' do - let(:import_type) { 'following' } - let(:import_file) { 'imports.txt' } - - before do - stub_const 'Form::Import::FILE_SIZE_LIMIT', 5 - end - - it 'has errors' do - subject.validate - expect(subject.errors[:data]).to include(I18n.t('imports.errors.too_large')) - end - end - - context 'when the CSV file is malformed CSV' do - let(:import_type) { 'following' } - let(:import_file) { 'boop.ogg' } - - it 'has errors' do - # NOTE: not testing more specific error because we don't know the string to match - expect(subject).to model_have_error_on_field(:data) - end - end - - context 'when importing more follows than allowed' do - let(:import_type) { 'following' } - let(:import_file) { 'imports.txt' } - - before do - allow(FollowLimitValidator).to receive(:limit_for_account).with(account).and_return(1) - end - - it 'has errors' do - subject.validate - expect(subject.errors[:data]).to include(I18n.t('users.follow_limit_reached', limit: 1)) - end - end - - it_behaves_like 'too many CSV rows', 'following', 'imports.txt', 1 - it_behaves_like 'too many CSV rows', 'blocking', 'imports.txt', 1 - it_behaves_like 'too many CSV rows', 'muting', 'imports.txt', 1 - it_behaves_like 'too many CSV rows', 'domain_blocking', 'domain_blocks.csv', 2 - it_behaves_like 'too many CSV rows', 'bookmarks', 'bookmark-imports.txt', 3 - it_behaves_like 'too many CSV rows', 'lists', 'lists.csv', 2 - - # Importing list of addresses with no headers into various types - it_behaves_like 'valid import', 'following', 'imports.txt' - it_behaves_like 'valid import', 'blocking', 'imports.txt' - it_behaves_like 'valid import', 'muting', 'imports.txt' - - # Importing domain blocks with headers into expected type - it_behaves_like 'valid import', 'domain_blocking', 'domain_blocks.csv' - - # Importing bookmarks list with no headers into expected type - it_behaves_like 'valid import', 'bookmarks', 'bookmark-imports.txt' - - # Importing lists with no headers into expected type - it_behaves_like 'valid import', 'lists', 'lists.csv' - - # Importing followed accounts with headers into various compatible types - it_behaves_like 'valid import', 'following', 'following_accounts.csv' - it_behaves_like 'valid import', 'blocking', 'following_accounts.csv' - it_behaves_like 'valid import', 'muting', 'following_accounts.csv' - - # Importing domain blocks with headers into incompatible types - it_behaves_like 'incompatible import type', 'following', 'domain_blocks.csv' - it_behaves_like 'incompatible import type', 'blocking', 'domain_blocks.csv' - it_behaves_like 'incompatible import type', 'muting', 'domain_blocks.csv' - it_behaves_like 'incompatible import type', 'bookmarks', 'domain_blocks.csv' - - # Importing followed accounts with headers into incompatible types - it_behaves_like 'incompatible import type', 'domain_blocking', 'following_accounts.csv' - it_behaves_like 'incompatible import type', 'bookmarks', 'following_accounts.csv' - end - - describe '#guessed_type' do - shared_examples 'with enough information' do |type, file, original_filename, expected_guess| - let(:import_file) { file } - let(:import_type) { type } - - before do - allow(data).to receive(:original_filename).and_return(original_filename) - end - - it 'guesses the expected type' do - expect(subject.guessed_type).to eq expected_guess - end - end - - context 'when the headers are enough to disambiguate' do - it_behaves_like 'with enough information', 'following', 'following_accounts.csv', 'import.csv', :following - it_behaves_like 'with enough information', 'blocking', 'following_accounts.csv', 'import.csv', :following - it_behaves_like 'with enough information', 'muting', 'following_accounts.csv', 'import.csv', :following - - it_behaves_like 'with enough information', 'following', 'muted_accounts.csv', 'imports.csv', :muting - it_behaves_like 'with enough information', 'blocking', 'muted_accounts.csv', 'imports.csv', :muting - it_behaves_like 'with enough information', 'muting', 'muted_accounts.csv', 'imports.csv', :muting - end - - context 'when the file name is enough to disambiguate' do - it_behaves_like 'with enough information', 'following', 'imports.txt', 'following_accounts.csv', :following - it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'following_accounts.csv', :following - it_behaves_like 'with enough information', 'muting', 'imports.txt', 'following_accounts.csv', :following - - it_behaves_like 'with enough information', 'following', 'imports.txt', 'follows.csv', :following - it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'follows.csv', :following - it_behaves_like 'with enough information', 'muting', 'imports.txt', 'follows.csv', :following - - it_behaves_like 'with enough information', 'following', 'imports.txt', 'blocked_accounts.csv', :blocking - it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'blocked_accounts.csv', :blocking - it_behaves_like 'with enough information', 'muting', 'imports.txt', 'blocked_accounts.csv', :blocking - - it_behaves_like 'with enough information', 'following', 'imports.txt', 'blocks.csv', :blocking - it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'blocks.csv', :blocking - it_behaves_like 'with enough information', 'muting', 'imports.txt', 'blocks.csv', :blocking - - it_behaves_like 'with enough information', 'following', 'imports.txt', 'muted_accounts.csv', :muting - it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'muted_accounts.csv', :muting - it_behaves_like 'with enough information', 'muting', 'imports.txt', 'muted_accounts.csv', :muting - - it_behaves_like 'with enough information', 'following', 'imports.txt', 'mutes.csv', :muting - it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'mutes.csv', :muting - it_behaves_like 'with enough information', 'muting', 'imports.txt', 'mutes.csv', :muting - end - end - - describe '#likely_mismatched?' do - shared_examples 'with matching types' do |type, file, original_filename = nil| - let(:import_file) { file } - let(:import_type) { type } - - before do - allow(data).to receive(:original_filename).and_return(original_filename) if original_filename.present? - end - - it 'returns false' do - expect(subject.likely_mismatched?).to be false - end - end - - shared_examples 'with mismatching types' do |type, file, original_filename = nil| - let(:import_file) { file } - let(:import_type) { type } - - before do - allow(data).to receive(:original_filename).and_return(original_filename) if original_filename.present? - end - - it 'returns true' do - expect(subject.likely_mismatched?).to be true - end - end - - it_behaves_like 'with matching types', 'following', 'following_accounts.csv' - it_behaves_like 'with matching types', 'following', 'following_accounts.csv', 'imports.txt' - it_behaves_like 'with matching types', 'following', 'imports.txt' - it_behaves_like 'with matching types', 'blocking', 'imports.txt', 'blocks.csv' - it_behaves_like 'with matching types', 'blocking', 'imports.txt' - it_behaves_like 'with matching types', 'muting', 'muted_accounts.csv' - it_behaves_like 'with matching types', 'muting', 'muted_accounts.csv', 'imports.txt' - it_behaves_like 'with matching types', 'muting', 'imports.txt' - it_behaves_like 'with matching types', 'domain_blocking', 'domain_blocks.csv' - it_behaves_like 'with matching types', 'domain_blocking', 'domain_blocks.csv', 'imports.txt' - it_behaves_like 'with matching types', 'bookmarks', 'bookmark-imports.txt' - it_behaves_like 'with matching types', 'bookmarks', 'bookmark-imports.txt', 'imports.txt' - - it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'blocks.csv' - it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'blocked_accounts.csv' - it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'mutes.csv' - it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'muted_accounts.csv' - it_behaves_like 'with mismatching types', 'following', 'muted_accounts.csv' - it_behaves_like 'with mismatching types', 'following', 'muted_accounts.csv', 'imports.txt' - it_behaves_like 'with mismatching types', 'blocking', 'following_accounts.csv' - it_behaves_like 'with mismatching types', 'blocking', 'following_accounts.csv', 'imports.txt' - it_behaves_like 'with mismatching types', 'blocking', 'muted_accounts.csv' - it_behaves_like 'with mismatching types', 'blocking', 'muted_accounts.csv', 'imports.txt' - it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'follows.csv' - it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'following_accounts.csv' - it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'mutes.csv' - it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'muted_accounts.csv' - it_behaves_like 'with mismatching types', 'muting', 'following_accounts.csv' - it_behaves_like 'with mismatching types', 'muting', 'following_accounts.csv', 'imports.txt' - it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'follows.csv' - it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'following_accounts.csv' - it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'blocks.csv' - it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'blocked_accounts.csv' - end - - describe 'save' do - shared_examples 'on successful import' do |type, mode, file, expected_rows| - let(:import_type) { type } - let(:import_file) { file } - let(:import_mode) { mode } - - before do - subject.save - end - - it 'creates the expected rows' do - expect(account.bulk_imports.first.rows.pluck(:data)).to match_array(expected_rows) - end - - context 'with a BulkImport' do - let(:bulk_import) { account.bulk_imports.first } - - it 'creates a non-nil bulk import' do - expect(bulk_import).to_not be_nil - end - - it 'matches the subjects type' do - expect(bulk_import.type.to_sym).to eq subject.type.to_sym - end - - it 'matches the subjects original filename' do - expect(bulk_import.original_filename).to eq subject.data.original_filename - end - - it 'matches the subjects likely_mismatched? value' do - expect(bulk_import.likely_mismatched?).to eq subject.likely_mismatched? - end - - it 'matches the subject overwrite value' do - expect(bulk_import.overwrite?).to eq !!subject.overwrite # rubocop:disable Style/DoubleNegation - end - - it 'has zero processed items' do - expect(bulk_import.processed_items).to eq 0 - end - - it 'has zero imported items' do - expect(bulk_import.imported_items).to eq 0 - end - - it 'has a correct total_items value' do - expect(bulk_import.total_items).to eq bulk_import.rows.count - end - - it 'defaults to unconfirmed true' do - expect(bulk_import.state_unconfirmed?).to be true - end - end - end - - it_behaves_like 'on successful import', 'following', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'following', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'blocking', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'blocking', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'muting', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'domain_blocking', 'merge', 'domain_blocks.csv', (%w(bad.domain worse.domain reject.media).map { |domain| { 'domain' => domain } }) - it_behaves_like 'on successful import', 'bookmarks', 'merge', 'bookmark-imports.txt', (%w(https://example.com/statuses/1312 https://local.com/users/foo/statuses/42 https://unknown-remote.com/users/bar/statuses/1 https://example.com/statuses/direct).map { |uri| { 'uri' => uri } }) - - it_behaves_like 'on successful import', 'following', 'merge', 'following_accounts.csv', [ - { 'acct' => 'user@example.com', 'show_reblogs' => true, 'notify' => false, 'languages' => nil }, - { 'acct' => 'user@test.com', 'show_reblogs' => true, 'notify' => true, 'languages' => %w(en fr) }, - ] - - it_behaves_like 'on successful import', 'muting', 'merge', 'muted_accounts.csv', [ - { 'acct' => 'user@example.com', 'hide_notifications' => true }, - { 'acct' => 'user@test.com', 'hide_notifications' => false }, - ] - - it_behaves_like 'on successful import', 'lists', 'merge', 'lists.csv', [ - { 'acct' => 'gargron@example.com', 'list_name' => 'Mastodon project' }, - { 'acct' => 'mastodon@example.com', 'list_name' => 'Mastodon project' }, - { 'acct' => 'foo@example.com', 'list_name' => 'test' }, - ] - - # Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users - # - # https://github.com/mastodon/mastodon/issues/20571 - it_behaves_like 'on successful import', 'following', 'merge', 'utf8-followers.txt', [{ 'acct' => 'nare@թութ.հայ' }] - end -end diff --git a/spec/models/form/status_filter_batch_action_spec.rb b/spec/models/form/status_filter_batch_action_spec.rb deleted file mode 100644 index f06a11cc8b..0000000000 --- a/spec/models/form/status_filter_batch_action_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Form::StatusFilterBatchAction do - describe '#save!' do - it 'does nothing if status_filter_ids is empty' do - batch_action = described_class.new(status_filter_ids: []) - - expect(batch_action.save!).to be_nil - end - end -end diff --git a/spec/models/home_feed_spec.rb b/spec/models/home_feed_spec.rb deleted file mode 100644 index 06bb63b1a4..0000000000 --- a/spec/models/home_feed_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe HomeFeed do - subject { described_class.new(account) } - - let(:account) { Fabricate(:account) } - - describe '#get' do - before do - Fabricate(:status, account: account, id: 1) - Fabricate(:status, account: account, id: 2) - Fabricate(:status, account: account, id: 3) - Fabricate(:status, account: account, id: 10) - end - - context 'when feed is generated' do - before do - redis.zadd( - FeedManager.instance.key(:home, account.id), - [[4, 4], [3, 3], [2, 2], [1, 1]] - ) - end - - it 'gets statuses with ids in the range from redis' do - results = subject.get(3) - - expect(results.map(&:id)).to eq [3, 2] - end - end - - context 'when feed is being generated' do - before do - redis.set("account:#{account.id}:regeneration", true) - end - - it 'returns nothing' do - results = subject.get(3) - - expect(results.map(&:id)).to eq [] - end - end - end -end diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb deleted file mode 100644 index d5a2ffbc86..0000000000 --- a/spec/models/identity_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Identity do - describe '.find_for_omniauth' do - let(:auth) { Fabricate(:identity, user: Fabricate(:user)) } - - it 'calls .find_or_create_by' do - allow(described_class).to receive(:find_or_create_by) - - described_class.find_for_omniauth(auth) - - expect(described_class).to have_received(:find_or_create_by).with(uid: auth.uid, provider: auth.provider) - end - - it 'returns an instance of Identity' do - expect(described_class.find_for_omniauth(auth)).to be_instance_of described_class - end - end -end diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb deleted file mode 100644 index 10df5f8c0b..0000000000 --- a/spec/models/import_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Import do - let(:account) { Fabricate(:account) } - let(:type) { 'following' } - let(:data) { attachment_fixture('imports.txt') } - - describe 'validations' do - it 'is invalid without an type' do - import = described_class.create(account: account, data: data) - expect(import).to model_have_error_on_field(:type) - end - - it 'is invalid without a data' do - import = described_class.create(account: account, type: type) - expect(import).to model_have_error_on_field(:data) - end - end -end diff --git a/spec/models/instance_spec.rb b/spec/models/instance_spec.rb deleted file mode 100644 index 3e811d3325..0000000000 --- a/spec/models/instance_spec.rb +++ /dev/null @@ -1,104 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Instance do - describe 'Scopes' do - before { described_class.refresh } - - describe '#searchable' do - let(:expected_domain) { 'host.example' } - let(:blocked_domain) { 'other.example' } - - before do - Fabricate :account, domain: expected_domain - Fabricate :account, domain: blocked_domain - Fabricate :domain_block, domain: blocked_domain - end - - it 'returns records not domain blocked' do - results = described_class.searchable.pluck(:domain) - - expect(results) - .to include(expected_domain) - .and not_include(blocked_domain) - end - end - - describe '#matches_domain' do - let(:host_domain) { 'host.example.com' } - let(:host_under_domain) { 'host_under.example.com' } - let(:other_domain) { 'other.example' } - - before do - Fabricate :account, domain: host_domain - Fabricate :account, domain: host_under_domain - Fabricate :account, domain: other_domain - end - - it 'returns matching records' do - expect(described_class.matches_domain('host.exa').pluck(:domain)) - .to include(host_domain) - .and not_include(other_domain) - - expect(described_class.matches_domain('ple.com').pluck(:domain)) - .to include(host_domain) - .and not_include(other_domain) - - expect(described_class.matches_domain('example').pluck(:domain)) - .to include(host_domain) - .and include(other_domain) - - expect(described_class.matches_domain('host_').pluck(:domain)) # Preserve SQL wildcards - .to include(host_domain) - .and include(host_under_domain) - .and not_include(other_domain) - end - end - - describe '#by_domain_and_subdomains' do - let(:exact_match_domain) { 'example.com' } - let(:subdomain_domain) { 'foo.example.com' } - let(:partial_domain) { 'grexample.com' } - - before do - Fabricate(:account, domain: exact_match_domain) - Fabricate(:account, domain: subdomain_domain) - Fabricate(:account, domain: partial_domain) - end - - it 'returns matching instances' do - results = described_class.by_domain_and_subdomains('example.com').pluck(:domain) - - expect(results) - .to include(exact_match_domain) - .and include(subdomain_domain) - .and not_include(partial_domain) - end - end - - describe '#with_domain_follows' do - let(:example_domain) { 'example.host' } - let(:other_domain) { 'other.host' } - let(:none_domain) { 'none.host' } - - before do - example_account = Fabricate(:account, domain: example_domain) - other_account = Fabricate(:account, domain: other_domain) - Fabricate(:account, domain: none_domain) - - Fabricate :follow, account: example_account - Fabricate :follow, target_account: other_account - end - - it 'returns instances with domain accounts that have follows' do - results = described_class.with_domain_follows(['example.host', 'other.host', 'none.host']).pluck(:domain) - - expect(results) - .to include(example_domain) - .and include(other_domain) - .and not_include(none_domain) - end - end - end -end diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb deleted file mode 100644 index 4ad589f2c7..0000000000 --- a/spec/models/invite_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Invite do - describe '#valid_for_use?' do - it 'returns true when there are no limitations' do - invite = Fabricate(:invite, max_uses: nil, expires_at: nil) - expect(invite.valid_for_use?).to be true - end - - it 'returns true when not expired' do - invite = Fabricate(:invite, max_uses: nil, expires_at: 1.hour.from_now) - expect(invite.valid_for_use?).to be true - end - - it 'returns false when expired' do - invite = Fabricate(:invite, max_uses: nil, expires_at: 1.hour.ago) - expect(invite.valid_for_use?).to be false - end - - it 'returns true when uses still available' do - invite = Fabricate(:invite, max_uses: 250, uses: 249, expires_at: nil) - expect(invite.valid_for_use?).to be true - end - - it 'returns false when maximum uses reached' do - invite = Fabricate(:invite, max_uses: 250, uses: 250, expires_at: nil) - expect(invite.valid_for_use?).to be false - end - - it 'returns false when invite creator has been disabled' do - invite = Fabricate(:invite, max_uses: nil, expires_at: nil) - invite.user.account.suspend! - expect(invite.valid_for_use?).to be false - end - end -end diff --git a/spec/models/ip_block_spec.rb b/spec/models/ip_block_spec.rb deleted file mode 100644 index 290b99b288..0000000000 --- a/spec/models/ip_block_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe IpBlock do - describe 'validations' do - it 'validates ip presence', :aggregate_failures do - ip_block = described_class.new(ip: nil, severity: :no_access) - - expect(ip_block).to_not be_valid - expect(ip_block).to model_have_error_on_field(:ip) - end - - it 'validates severity presence', :aggregate_failures do - ip_block = described_class.new(ip: '127.0.0.1', severity: nil) - - expect(ip_block).to_not be_valid - expect(ip_block).to model_have_error_on_field(:severity) - end - - it 'validates ip uniqueness', :aggregate_failures do - described_class.create!(ip: '127.0.0.1', severity: :no_access) - - ip_block = described_class.new(ip: '127.0.0.1', severity: :no_access) - - expect(ip_block).to_not be_valid - expect(ip_block).to model_have_error_on_field(:ip) - end - end - - 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 - - describe '.blocked?' do - context 'when the IP is blocked' do - it 'returns true' do - described_class.create!(ip: '127.0.0.1', severity: :no_access) - - expect(described_class.blocked?('127.0.0.1')).to be true - end - end - - context 'when the IP is not blocked' do - it 'returns false' do - expect(described_class.blocked?('127.0.0.1')).to be false - end - end - end - - describe 'after_commit' do - it 'resets the cache' do - allow(Rails.cache).to receive(:delete) - - described_class.create!(ip: '127.0.0.1', severity: :no_access) - - expect(Rails.cache).to have_received(:delete).with(described_class::CACHE_KEY) - end - end -end diff --git a/spec/models/marker_spec.rb b/spec/models/marker_spec.rb deleted file mode 100644 index 51dd584388..0000000000 --- a/spec/models/marker_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -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/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb deleted file mode 100644 index 3142b291fb..0000000000 --- a/spec/models/media_attachment_spec.rb +++ /dev/null @@ -1,300 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe MediaAttachment, :attachment_processing do - describe 'local?' do - subject { media_attachment.local? } - - let(:media_attachment) { described_class.new(remote_url: remote_url) } - - context 'when remote_url is blank' do - let(:remote_url) { '' } - - it 'returns true' do - expect(subject).to be true - end - end - - context 'when remote_url is present' do - let(:remote_url) { 'remote_url' } - - it 'returns false' do - expect(subject).to be false - end - end - end - - describe 'needs_redownload?' do - subject { media_attachment.needs_redownload? } - - let(:media_attachment) { described_class.new(remote_url: remote_url, file: file) } - - context 'when file is blank' do - let(:file) { nil } - - context 'when remote_url is present' do - let(:remote_url) { 'remote_url' } - - it 'returns true' do - expect(subject).to be true - end - end - end - - context 'when file is present' do - let(:file) { attachment_fixture('avatar.gif') } - - context 'when remote_url is blank' do - let(:remote_url) { '' } - - it 'returns false' do - expect(subject).to be false - end - end - - context 'when remote_url is present' do - let(:remote_url) { 'remote_url' } - - it 'returns true' do - expect(subject).to be false - end - end - end - end - - describe '#to_param' do - let(:media_attachment) { Fabricate.build(:media_attachment, shortcode: shortcode, id: id) } - - context 'when media attachment has a shortcode' do - let(:shortcode) { 'foo' } - let(:id) { 123 } - - it 'returns shortcode' do - expect(media_attachment.to_param).to eq shortcode - end - end - - context 'when media attachment does not have a shortcode' do - let(:shortcode) { nil } - let(:id) { 123 } - - it 'returns string representation of id' do - expect(media_attachment.to_param).to eq id.to_s - end - end - end - - shared_examples 'static 600x400 image' do |content_type, extension| - after do - media.destroy - end - - it 'saves media attachment with correct file and size metadata' do - expect(media) - .to be_persisted - .and be_processing_complete - .and have_attributes( - file: be_present, - type: eq('image'), - file_content_type: eq(content_type), - file_file_name: end_with(extension) - ) - - # Rack::Mime (used by PublicFileServerMiddleware) recognizes file extension - expect(Rack::Mime.mime_type(extension, nil)).to eq content_type - - # Strip original file name - expect(media.file_file_name) - .to_not start_with '600x400' - - # Set meta for original and thumbnail - expect(media.file.meta.deep_symbolize_keys) - .to include( - original: include( - width: eq(600), - height: eq(400), - aspect: eq(1.5) - ), - small: include( - width: eq(588), - height: eq(392), - aspect: eq(1.5) - ) - ) - end - end - - describe 'jpeg' do - let(:media) { Fabricate(:media_attachment, file: attachment_fixture('600x400.jpeg')) } - - it_behaves_like 'static 600x400 image', 'image/jpeg', '.jpeg' - end - - describe 'png' do - let(:media) { Fabricate(:media_attachment, file: attachment_fixture('600x400.png')) } - - it_behaves_like 'static 600x400 image', 'image/png', '.png' - end - - describe 'monochrome jpg' do - let(:media) { Fabricate(:media_attachment, file: attachment_fixture('monochrome.png')) } - - it_behaves_like 'static 600x400 image', 'image/png', '.png' - end - - describe 'webp' do - let(:media) { Fabricate(:media_attachment, file: attachment_fixture('600x400.webp')) } - - it_behaves_like 'static 600x400 image', 'image/webp', '.webp' - end - - describe 'avif' do - let(:media) { Fabricate(:media_attachment, file: attachment_fixture('600x400.avif')) } - - it_behaves_like 'static 600x400 image', 'image/jpeg', '.jpeg' - end - - describe 'heic' do - let(:media) { Fabricate(:media_attachment, file: attachment_fixture('600x400.heic')) } - - it_behaves_like 'static 600x400 image', 'image/jpeg', '.jpeg' - end - - describe 'base64-encoded image' do - let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('600x400.jpeg').read)}" } - let(:media) { Fabricate(:media_attachment, file: base64_attachment) } - - it_behaves_like 'static 600x400 image', 'image/jpeg', '.jpeg' - end - - describe 'animated gif' do - let(:media) { Fabricate(:media_attachment, file: attachment_fixture('avatar.gif')) } - - it 'sets correct file metadata' do - expect(media) - .to have_attributes( - type: eq('gifv'), - file_content_type: eq('video/mp4') - ) - expect(media_metadata) - .to include( - original: include( - width: eq(128), - height: eq(128) - ) - ) - end - end - - describe 'static gif' do - fixtures = [ - { filename: 'attachment.gif', width: 600, height: 400, aspect: 1.5 }, - { filename: 'mini-static.gif', width: 32, height: 32, aspect: 1.0 }, - ] - - fixtures.each do |fixture| - context fixture[:filename] do - let(:media) { Fabricate(:media_attachment, file: attachment_fixture(fixture[:filename])) } - - it 'sets correct file metadata' do - expect(media) - .to have_attributes( - type: eq('image'), - file_content_type: eq('image/gif') - ) - expect(media_metadata) - .to include( - original: include( - width: eq(fixture[:width]), - height: eq(fixture[:height]), - aspect: eq(fixture[:aspect]) - ) - ) - end - end - end - end - - describe 'ogg with cover art' do - let(:media) { Fabricate(:media_attachment, file: attachment_fixture('boop.ogg')) } - let(:expected_media_duration) { 0.235102 } - - # The libvips and ImageMagick implementations produce different results - let(:expected_background_color) { Rails.configuration.x.use_vips ? '#268cd9' : '#3088d4' } - - it 'sets correct file metadata' do - expect(media) - .to have_attributes( - type: eq('audio'), - thumbnail: be_present, - file_file_name: not_eq('boop.ogg') - ) - - expect(media_metadata) - .to include( - original: include(duration: be_within(0.05).of(expected_media_duration)), - colors: include(background: eq(expected_background_color)) - ) - end - end - - describe 'mp3 with large cover art' do - let(:media) { Fabricate(:media_attachment, file: attachment_fixture('boop.mp3')) } - let(:expected_media_duration) { 0.235102 } - - it 'detects file type and sets correct metadata' do - expect(media) - .to have_attributes( - type: eq('audio'), - thumbnail: be_present, - file_file_name: not_eq('boop.mp3') - ) - expect(media_metadata) - .to include( - original: include(duration: be_within(0.05).of(expected_media_duration)) - ) - end - end - - it 'is invalid without file' do - media = described_class.new - - expect(media.valid?).to be false - expect(media).to model_have_error_on_field(:file) - end - - describe 'size limit validation' do - it 'rejects video files that are too large' do - stub_const 'MediaAttachment::IMAGE_LIMIT', 100.megabytes - stub_const 'MediaAttachment::VIDEO_LIMIT', 1.kilobyte - expect { Fabricate(:media_attachment, file: attachment_fixture('attachment.webm')) }.to raise_error(ActiveRecord::RecordInvalid) - end - - it 'accepts video files that are small enough' do - stub_const 'MediaAttachment::IMAGE_LIMIT', 1.kilobyte - stub_const 'MediaAttachment::VIDEO_LIMIT', 100.megabytes - media = Fabricate(:media_attachment, file: attachment_fixture('attachment.webm')) - expect(media.valid?).to be true - end - - it 'rejects image files that are too large' do - stub_const 'MediaAttachment::IMAGE_LIMIT', 1.kilobyte - stub_const 'MediaAttachment::VIDEO_LIMIT', 100.megabytes - expect { Fabricate(:media_attachment, file: attachment_fixture('attachment.jpg')) }.to raise_error(ActiveRecord::RecordInvalid) - end - - it 'accepts image files that are small enough' do - stub_const 'MediaAttachment::IMAGE_LIMIT', 100.megabytes - stub_const 'MediaAttachment::VIDEO_LIMIT', 1.kilobyte - media = Fabricate(:media_attachment, file: attachment_fixture('attachment.jpg')) - expect(media.valid?).to be true - end - end - - private - - def media_metadata - media.file.meta.deep_symbolize_keys - end -end diff --git a/spec/models/mention_spec.rb b/spec/models/mention_spec.rb deleted file mode 100644 index b241049a54..0000000000 --- a/spec/models/mention_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Mention do - describe 'validations' do - it 'is invalid without an account' do - mention = Fabricate.build(:mention, account: nil) - mention.valid? - expect(mention).to model_have_error_on_field(:account) - end - - it 'is invalid without a status' do - mention = Fabricate.build(:mention, status: nil) - mention.valid? - expect(mention).to model_have_error_on_field(:status) - end - end -end diff --git a/spec/models/notification_policy_spec.rb b/spec/models/notification_policy_spec.rb deleted file mode 100644 index cfd8e85eda..0000000000 --- a/spec/models/notification_policy_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe NotificationPolicy do - describe '#summarize!' do - subject { Fabricate(:notification_policy) } - - let(:sender) { Fabricate(:account) } - - before do - Fabricate.times(2, :notification, account: subject.account, activity: Fabricate(:status, account: sender), filtered: true) - Fabricate(:notification_request, account: subject.account, from_account: sender) - subject.summarize! - end - - it 'sets pending_requests_count' do - expect(subject.pending_requests_count).to eq 1 - end - - it 'sets pending_notifications_count' do - expect(subject.pending_notifications_count).to eq 2 - end - end -end diff --git a/spec/models/notification_request_spec.rb b/spec/models/notification_request_spec.rb deleted file mode 100644 index 4adddc194f..0000000000 --- a/spec/models/notification_request_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe NotificationRequest do - describe '#reconsider_existence!' do - subject { Fabricate(:notification_request) } - - context 'when there are remaining notifications' do - before do - Fabricate(:notification, account: subject.account, activity: Fabricate(:status, account: subject.from_account), filtered: true) - subject.reconsider_existence! - end - - it 'leaves request intact' do - expect(subject.destroyed?).to be false - end - - it 'updates notifications_count' do - expect(subject.notifications_count).to eq 1 - end - end - - context 'when there are no notifications' do - before do - subject.reconsider_existence! - end - - it 'removes the request' do - expect(subject.destroyed?).to be true - end - end - end -end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb deleted file mode 100644 index d498ee02a5..0000000000 --- a/spec/models/notification_spec.rb +++ /dev/null @@ -1,339 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Notification do - describe '#target_status' do - let(:notification) { Fabricate(:notification, activity: activity) } - let(:status) { Fabricate(:status) } - let(:reblog) { Fabricate(:status, reblog: status) } - let(:favourite) { Fabricate(:favourite, status: status) } - let(:mention) { Fabricate(:mention, status: status) } - - context 'when Activity is reblog' do - let(:activity) { reblog } - - it 'returns status' do - expect(notification.target_status).to eq status - end - end - - context 'when Activity is favourite' do - let(:type) { :favourite } - let(:activity) { favourite } - - it 'returns status' do - expect(notification.target_status).to eq status - end - end - - context 'when Activity is mention' do - let(:activity) { mention } - - it 'returns status' do - expect(notification.target_status).to eq status - end - end - end - - describe '#type' do - it 'returns :reblog for a Status' do - notification = described_class.new(activity: Status.new) - expect(notification.type).to eq :reblog - end - - it 'returns :mention for a Mention' do - notification = described_class.new(activity: Mention.new) - expect(notification.type).to eq :mention - end - - it 'returns :favourite for a Favourite' do - notification = described_class.new(activity: Favourite.new) - expect(notification.type).to eq :favourite - end - - it 'returns :follow for a Follow' do - notification = described_class.new(activity: Follow.new) - expect(notification.type).to eq :follow - end - end - - describe 'Setting account from activity_type' do - context 'when activity_type is a Status' do - it 'sets the notification from_account correctly' do - status = Fabricate(:status) - - notification = Fabricate.build(:notification, activity_type: 'Status', activity: status) - - expect(notification.from_account).to eq(status.account) - end - end - - context 'when activity_type is a Follow' do - it 'sets the notification from_account correctly' do - follow = Fabricate(:follow) - - notification = Fabricate.build(:notification, activity_type: 'Follow', activity: follow) - - expect(notification.from_account).to eq(follow.account) - end - end - - context 'when activity_type is a Favourite' do - it 'sets the notification from_account correctly' do - favourite = Fabricate(:favourite) - - notification = Fabricate.build(:notification, activity_type: 'Favourite', activity: favourite) - - expect(notification.from_account).to eq(favourite.account) - end - end - - context 'when activity_type is a FollowRequest' do - it 'sets the notification from_account correctly' do - follow_request = Fabricate(:follow_request) - - notification = Fabricate.build(:notification, activity_type: 'FollowRequest', activity: follow_request) - - expect(notification.from_account).to eq(follow_request.account) - end - end - - context 'when activity_type is a Poll' do - it 'sets the notification from_account correctly' do - poll = Fabricate(:poll) - - notification = Fabricate.build(:notification, activity_type: 'Poll', activity: poll) - - expect(notification.from_account).to eq(poll.account) - end - end - - context 'when activity_type is a Report' do - it 'sets the notification from_account correctly' do - report = Fabricate(:report) - - notification = Fabricate.build(:notification, activity_type: 'Report', activity: report) - - expect(notification.from_account).to eq(report.account) - end - end - - context 'when activity_type is a Mention' do - it 'sets the notification from_account correctly' do - mention = Fabricate(:mention) - - notification = Fabricate.build(:notification, activity_type: 'Mention', activity: mention) - - expect(notification.from_account).to eq(mention.status.account) - end - end - - context 'when activity_type is an Account' do - it 'sets the notification from_account correctly' do - account = Fabricate(:account) - - notification = Fabricate.build(:notification, activity_type: 'Account', account: account) - - expect(notification.account).to eq(account) - end - end - - context 'when activity_type is an AccountWarning' do - it 'sets the notification from_account to the recipient of the notification' do - account = Fabricate(:account) - account_warning = Fabricate(:account_warning, target_account: account) - - notification = Fabricate.build(:notification, activity_type: 'AccountWarning', activity: account_warning, account: account) - - expect(notification.from_account).to eq(account) - end - end - end - - describe '.paginate_groups_by_max_id' do - let(:account) { Fabricate(:account) } - - let!(:notifications) do - ['group-1', 'group-1', nil, 'group-2', nil, 'group-1', 'group-2', 'group-1'] - .map { |group_key| Fabricate(:notification, account: account, group_key: group_key) } - end - - context 'without since_id or max_id' do - it 'returns the most recent notifications, only keeping one notification per group' do - expect(described_class.without_suspended.paginate_groups_by_max_id(4).pluck(:id)) - .to eq [notifications[7], notifications[6], notifications[4], notifications[2]].pluck(:id) - end - end - - context 'with since_id' do - it 'returns the most recent notifications, only keeping one notification per group' do - expect(described_class.without_suspended.paginate_groups_by_max_id(4, since_id: notifications[4].id).pluck(:id)) - .to eq [notifications[7], notifications[6]].pluck(:id) - end - end - - context 'with max_id' do - it 'returns the most recent notifications after max_id, only keeping one notification per group' do - expect(described_class.without_suspended.paginate_groups_by_max_id(4, max_id: notifications[7].id).pluck(:id)) - .to eq [notifications[6], notifications[5], notifications[4], notifications[2]].pluck(:id) - end - end - end - - describe '.paginate_groups_by_min_id' do - let(:account) { Fabricate(:account) } - - let!(:notifications) do - ['group-1', 'group-1', nil, 'group-2', nil, 'group-1', 'group-2', 'group-1'] - .map { |group_key| Fabricate(:notification, account: account, group_key: group_key) } - end - - context 'without min_id or max_id' do - it 'returns the oldest notifications, only keeping one notification per group' do - expect(described_class.without_suspended.paginate_groups_by_min_id(4).pluck(:id)) - .to eq [notifications[0], notifications[2], notifications[3], notifications[4]].pluck(:id) - end - end - - context 'with max_id' do - it 'returns the oldest notifications, stopping at max_id, only keeping one notification per group' do - expect(described_class.without_suspended.paginate_groups_by_min_id(4, max_id: notifications[4].id).pluck(:id)) - .to eq [notifications[0], notifications[2], notifications[3]].pluck(:id) - end - end - - context 'with min_id' do - it 'returns the most oldest notifications after min_id, only keeping one notification per group' do - expect(described_class.without_suspended.paginate_groups_by_min_id(4, min_id: notifications[0].id).pluck(:id)) - .to eq [notifications[1], notifications[2], notifications[3], notifications[4]].pluck(:id) - end - end - end - - describe '.preload_cache_collection_target_statuses' do - subject do - described_class.preload_cache_collection_target_statuses(notifications) do |target_statuses| - # preload account for testing instead of using cache_collection - Status.preload(:account).where(id: target_statuses.map(&:id)) - end - end - - context 'when notifications are empty' do - let(:notifications) { [] } - - it 'returns []' do - expect(subject).to eq [] - end - end - - context 'when notifications are present' do - before do - notifications.each(&:reload) - end - - let(:mention) { Fabricate(:mention) } - let(:status) { Fabricate(:status) } - let(:reblog) { Fabricate(:status, reblog: Fabricate(:status)) } - let(:follow) { Fabricate(:follow) } - let(:follow_request) { Fabricate(:follow_request) } - let(:favourite) { Fabricate(:favourite) } - let(:poll) { Fabricate(:poll) } - - let(:notifications) do - [ - Fabricate(:notification, type: :mention, activity: mention), - Fabricate(:notification, type: :status, activity: status), - Fabricate(:notification, type: :reblog, activity: reblog), - Fabricate(:notification, type: :follow, activity: follow), - Fabricate(:notification, type: :follow_request, activity: follow_request), - Fabricate(:notification, type: :favourite, activity: favourite), - Fabricate(:notification, type: :poll, activity: poll), - ] - end - - context 'with a preloaded target status' do - it 'preloads mention' do - expect(subject[0].type).to eq :mention - expect(subject[0].association(:mention)).to be_loaded - expect(subject[0].mention.association(:status)).to be_loaded - end - - it 'preloads status' do - expect(subject[1].type).to eq :status - expect(subject[1].association(:status)).to be_loaded - end - - it 'preloads reblog' do - expect(subject[2].type).to eq :reblog - expect(subject[2].association(:status)).to be_loaded - expect(subject[2].status.association(:reblog)).to be_loaded - end - - it 'preloads follow as nil' do - expect(subject[3].type).to eq :follow - expect(subject[3].target_status).to be_nil - end - - it 'preloads follow_request as nill' do - expect(subject[4].type).to eq :follow_request - expect(subject[4].target_status).to be_nil - end - - it 'preloads favourite' do - expect(subject[5].type).to eq :favourite - expect(subject[5].association(:favourite)).to be_loaded - expect(subject[5].favourite.association(:status)).to be_loaded - end - - it 'preloads poll' do - expect(subject[6].type).to eq :poll - expect(subject[6].association(:poll)).to be_loaded - expect(subject[6].poll.association(:status)).to be_loaded - end - end - - context 'with a cached status' do - it 'replaces mention' do - expect(subject[0].type).to eq :mention - expect(subject[0].target_status.association(:account)).to be_loaded - expect(subject[0].target_status).to eq mention.status - end - - it 'replaces status' do - expect(subject[1].type).to eq :status - expect(subject[1].target_status.association(:account)).to be_loaded - expect(subject[1].target_status).to eq status - end - - it 'replaces reblog' do - expect(subject[2].type).to eq :reblog - expect(subject[2].target_status.association(:account)).to be_loaded - expect(subject[2].target_status).to eq reblog.reblog - end - - it 'replaces follow' do - expect(subject[3].type).to eq :follow - expect(subject[3].target_status).to be_nil - end - - it 'replaces follow_request' do - expect(subject[4].type).to eq :follow_request - expect(subject[4].target_status).to be_nil - end - - it 'replaces favourite' do - expect(subject[5].type).to eq :favourite - expect(subject[5].target_status.association(:account)).to be_loaded - expect(subject[5].target_status).to eq favourite.status - end - - it 'replaces poll' do - expect(subject[6].type).to eq :poll - expect(subject[6].target_status.association(:account)).to be_loaded - expect(subject[6].target_status).to eq poll.status - end - end - end - end -end diff --git a/spec/models/one_time_key_spec.rb b/spec/models/one_time_key_spec.rb deleted file mode 100644 index 6ff7ffc5c1..0000000000 --- a/spec/models/one_time_key_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -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 deleted file mode 100644 index ebcc459078..0000000000 --- a/spec/models/poll_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -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 - - describe 'validations' do - context 'when not valid' do - let(:poll) { Fabricate.build(:poll, expires_at: nil) } - - it 'is invalid without an expire date' do - poll.valid? - expect(poll).to model_have_error_on_field(:expires_at) - end - end - end -end diff --git a/spec/models/poll_vote_spec.rb b/spec/models/poll_vote_spec.rb deleted file mode 100644 index b017ea5279..0000000000 --- a/spec/models/poll_vote_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe PollVote do - describe '#object_type' do - let(:poll_vote) { Fabricate.build(:poll_vote) } - - it 'returns :vote' do - expect(poll_vote.object_type).to eq :vote - end - end - - describe 'validations' do - context 'with a vote on an expired poll' do - it 'marks the vote invalid' do - poll = Fabricate.build(:poll, expires_at: 30.days.ago) - - vote = Fabricate.build(:poll_vote, poll: poll) - expect(vote).to_not be_valid - end - end - - context 'with invalid choices' do - it 'marks vote invalid with negative choice' do - poll = Fabricate.build(:poll) - - vote = Fabricate.build(:poll_vote, poll: poll, choice: -100) - expect(vote).to_not be_valid - end - - it 'marks vote invalid with choice in excess of options' do - poll = Fabricate.build(:poll, options: %w(a b c)) - - vote = Fabricate.build(:poll_vote, poll: poll, choice: 10) - expect(vote).to_not be_valid - end - end - - context 'with a poll where multiple is true' do - it 'does not allow a second vote on same choice from same account' do - poll = Fabricate(:poll, multiple: true, options: %w(a b c)) - first_vote = Fabricate(:poll_vote, poll: poll, choice: 1) - expect(first_vote).to be_valid - - second_vote = Fabricate.build(:poll_vote, account: first_vote.account, poll: poll, choice: 1) - expect(second_vote).to_not be_valid - end - end - - context 'with a poll where multiple is false' do - it 'does not allow a second vote from same account' do - poll = Fabricate(:poll, multiple: false, options: %w(a b c)) - first_vote = Fabricate(:poll_vote, poll: poll) - expect(first_vote).to be_valid - - second_vote = Fabricate.build(:poll_vote, account: first_vote.account, poll: poll) - expect(second_vote).to_not be_valid - end - end - end -end diff --git a/spec/models/preview_card_provider_spec.rb b/spec/models/preview_card_provider_spec.rb deleted file mode 100644 index 7425b93946..0000000000 --- a/spec/models/preview_card_provider_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -# 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/preview_card_spec.rb b/spec/models/preview_card_spec.rb deleted file mode 100644 index a17c7532e9..0000000000 --- a/spec/models/preview_card_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe PreviewCard do - describe 'validations' do - describe 'urls' do - it 'allows http schemes' do - record = described_class.new(url: 'http://example.host/path') - - expect(record).to be_valid - end - - it 'allows https schemes' do - record = described_class.new(url: 'https://example.host/path') - - expect(record).to be_valid - end - - it 'does not allow javascript: schemes' do - record = described_class.new(url: 'javascript:alert()') - - expect(record).to_not be_valid - expect(record).to model_have_error_on_field(:url) - end - end - end -end diff --git a/spec/models/privacy_policy_spec.rb b/spec/models/privacy_policy_spec.rb deleted file mode 100644 index 03bbe7264b..0000000000 --- a/spec/models/privacy_policy_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# 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(described_class::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/public_feed_spec.rb b/spec/models/public_feed_spec.rb deleted file mode 100644 index 8a1a01e892..0000000000 --- a/spec/models/public_feed_spec.rb +++ /dev/null @@ -1,272 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe PublicFeed do - let(:account) { Fabricate(:account) } - - describe '#get' do - subject { described_class.new(nil).get(20).map(&:id) } - - it 'only includes statuses with public visibility' do - public_status = Fabricate(:status, visibility: :public) - private_status = Fabricate(:status, visibility: :private) - - expect(subject).to include(public_status.id) - expect(subject).to_not include(private_status.id) - end - - it 'does not include replies' do - status = Fabricate(:status) - reply = Fabricate(:status, in_reply_to_id: status.id) - - expect(subject).to include(status.id) - expect(subject).to_not include(reply.id) - end - - it 'does not include boosts' do - status = Fabricate(:status) - boost = Fabricate(:status, reblog_of_id: status.id) - - expect(subject).to include(status.id) - expect(subject).to_not include(boost.id) - end - - it 'filters out silenced accounts' do - silenced_account = Fabricate(:account, silenced: true) - status = Fabricate(:status, account: account) - silenced_status = Fabricate(:status, account: silenced_account) - - expect(subject).to include(status.id) - expect(subject).to_not include(silenced_status.id) - end - - context 'without local_only option' do - subject { described_class.new(viewer).get(20).map(&:id) } - - let(:viewer) { nil } - - let!(:local_account) { Fabricate(:account, domain: nil) } - let!(:remote_account) { Fabricate(:account, domain: 'test.com') } - let!(:local_status) { Fabricate(:status, account: local_account) } - let!(:remote_status) { Fabricate(:status, account: remote_account) } - let!(:local_only_status) { Fabricate(:status, account: local_account, local_only: true) } - - context 'without a viewer' do - let(:viewer) { nil } - - it 'includes remote instances statuses' do - expect(subject).to include(remote_status.id) - end - - it 'includes local statuses' do - expect(subject).to include(local_status.id) - end - - it 'does not include local-only statuses' do - expect(subject).to_not include(local_only_status.id) - end - end - - context 'with a viewer' do - let(:viewer) { Fabricate(:account, username: 'viewer') } - - it 'includes remote instances statuses' do - expect(subject).to include(remote_status.id) - end - - it 'includes local statuses' do - expect(subject).to include(local_status.id) - end - - it 'does not include local-only statuses' do - expect(subject).to_not include(local_only_status.id) - end - end - end - - context 'without local_only option but allow_local_only' do - subject { described_class.new(viewer, allow_local_only: true).get(20).map(&:id) } - - let(:viewer) { nil } - - let!(:local_account) { Fabricate(:account, domain: nil) } - let!(:remote_account) { Fabricate(:account, domain: 'test.com') } - let!(:local_status) { Fabricate(:status, account: local_account) } - let!(:remote_status) { Fabricate(:status, account: remote_account) } - let!(:local_only_status) { Fabricate(:status, account: local_account, local_only: true) } - - context 'without a viewer' do - let(:viewer) { nil } - - it 'includes remote instances statuses' do - expect(subject).to include(remote_status.id) - end - - it 'includes local statuses' do - expect(subject).to include(local_status.id) - end - - it 'does not include local-only statuses' do - expect(subject).to_not include(local_only_status.id) - end - end - - context 'with a viewer' do - let(:viewer) { Fabricate(:account, username: 'viewer') } - - it 'includes remote instances statuses' do - expect(subject).to include(remote_status.id) - end - - it 'includes local statuses' do - expect(subject).to include(local_status.id) - end - - it 'includes local-only statuses' do - expect(subject).to include(local_only_status.id) - end - end - end - - context 'with a local_only option set' do - subject { described_class.new(viewer, local: true).get(20).map(&:id) } - - let!(:local_account) { Fabricate(:account, domain: nil) } - let!(:remote_account) { Fabricate(:account, domain: 'test.com') } - let!(:local_status) { Fabricate(:status, account: local_account) } - let!(:remote_status) { Fabricate(:status, account: remote_account) } - let!(:local_only_status) { Fabricate(:status, account: local_account, local_only: true) } - - context 'without a viewer' do - let(:viewer) { nil } - - it 'does not include remote instances statuses' do - expect(subject).to include(local_status.id) - expect(subject).to_not include(remote_status.id) - end - - it 'does not include local-only statuses' do - expect(subject).to_not include(local_only_status.id) - end - end - - context 'with a viewer' do - let(:viewer) { Fabricate(:account, username: 'viewer') } - - it 'does not include remote instances statuses' do - expect(subject).to include(local_status.id) - expect(subject).to_not include(remote_status.id) - end - - it 'is not affected by personal domain blocks' do - viewer.block_domain!('test.com') - expect(subject).to include(local_status.id) - expect(subject).to_not include(remote_status.id) - end - - it 'includes local-only statuses' do - expect(subject).to include(local_only_status.id) - end - end - end - - context 'with a remote_only option set' do - subject { described_class.new(viewer, remote: true).get(20).map(&:id) } - - let!(:local_account) { Fabricate(:account, domain: nil) } - let!(:remote_account) { Fabricate(:account, domain: 'test.com') } - let!(:local_status) { Fabricate(:status, account: local_account) } - let!(:remote_status) { Fabricate(:status, account: remote_account) } - - context 'without a viewer' do - let(:viewer) { nil } - - it 'does not include local instances statuses' do - expect(subject).to_not include(local_status.id) - expect(subject).to include(remote_status.id) - end - end - - context 'with a viewer' do - let(:viewer) { Fabricate(:account, username: 'viewer') } - - it 'does not include local instances statuses' do - expect(subject).to_not include(local_status.id) - expect(subject).to include(remote_status.id) - end - end - end - - describe 'with an account passed in' do - subject { described_class.new(account).get(20).map(&:id) } - - let!(:account) { Fabricate(:account) } - - it 'excludes statuses from accounts blocked by the account' do - blocked = Fabricate(:account) - account.block!(blocked) - blocked_status = Fabricate(:status, account: blocked) - - expect(subject).to_not include(blocked_status.id) - end - - it 'excludes statuses from accounts who have blocked the account' do - blocker = Fabricate(:account) - blocker.block!(account) - blocked_status = Fabricate(:status, account: blocker) - - expect(subject).to_not include(blocked_status.id) - end - - it 'excludes statuses from accounts muted by the account' do - muted = Fabricate(:account) - account.mute!(muted) - muted_status = Fabricate(:status, account: muted) - - expect(subject).to_not include(muted_status.id) - end - - it 'excludes statuses from accounts from personally blocked domains' do - blocked = Fabricate(:account, domain: 'example.com') - account.block_domain!(blocked.domain) - blocked_status = Fabricate(:status, account: blocked) - - expect(subject).to_not include(blocked_status.id) - end - - context 'with language preferences' do - it 'excludes statuses in languages not allowed by the account user' do - account.user.update(chosen_languages: [:en, :es]) - en_status = Fabricate(:status, language: 'en') - es_status = Fabricate(:status, language: 'es') - fr_status = Fabricate(:status, language: 'fr') - - expect(subject).to include(en_status.id) - expect(subject).to include(es_status.id) - expect(subject).to_not include(fr_status.id) - end - - it 'includes all languages when user does not have a setting' do - account.user.update(chosen_languages: nil) - - en_status = Fabricate(:status, language: 'en') - es_status = Fabricate(:status, language: 'es') - - expect(subject).to include(en_status.id) - expect(subject).to include(es_status.id) - end - - it 'includes all languages when account does not have a user' do - account.update(user: nil) - - en_status = Fabricate(:status, language: 'en') - es_status = Fabricate(:status, language: 'es') - - expect(subject).to include(en_status.id) - expect(subject).to include(es_status.id) - end - end - end - end -end diff --git a/spec/models/relationship_filter_spec.rb b/spec/models/relationship_filter_spec.rb deleted file mode 100644 index fccd42aaad..0000000000 --- a/spec/models/relationship_filter_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe RelationshipFilter do - let(:account) { Fabricate(:account) } - - describe '#results' do - let(:account_of_7_months) { Fabricate(:account_stat, statuses_count: 1, last_status_at: 7.months.ago).account } - let(:account_of_1_day) { Fabricate(:account_stat, statuses_count: 1, last_status_at: 1.day.ago).account } - let(:account_of_3_days) { Fabricate(:account_stat, statuses_count: 1, last_status_at: 3.days.ago).account } - let(:silent_account) { Fabricate(:account_stat, statuses_count: 0, last_status_at: nil).account } - - before do - account.follow!(account_of_7_months) - account.follow!(account_of_1_day) - account.follow!(account_of_3_days) - account.follow!(silent_account) - end - - context 'when ordering by last activity' do - context 'when not filtering' do - subject do - described_class.new(account, 'order' => 'active').results - end - - it 'returns followings ordered by last activity' do - expect(subject).to eq [account_of_1_day, account_of_3_days, account_of_7_months, silent_account] - end - end - - context 'when filtering for dormant accounts' do - subject do - described_class.new(account, 'order' => 'active', 'activity' => 'dormant').results - end - - it 'returns dormant followings ordered by last activity' do - expect(subject).to eq [account_of_7_months, silent_account] - end - end - end - - context 'when ordering by account creation' do - context 'when not filtering' do - subject do - described_class.new(account, 'order' => 'recent').results - end - - it 'returns followings ordered by last account creation' do - expect(subject).to eq [silent_account, account_of_3_days, account_of_1_day, account_of_7_months] - end - end - - context 'when filtering for dormant accounts' do - subject do - described_class.new(account, 'order' => 'recent', 'activity' => 'dormant').results - end - - it 'returns dormant followings ordered by last activity' do - expect(subject).to eq [silent_account, account_of_7_months] - end - end - end - end -end diff --git a/spec/models/relationship_severance_event_spec.rb b/spec/models/relationship_severance_event_spec.rb deleted file mode 100644 index 93c0f1a26d..0000000000 --- a/spec/models/relationship_severance_event_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe RelationshipSeveranceEvent do - let(:local_account) { Fabricate(:account) } - let(:remote_account) { Fabricate(:account, domain: 'example.com') } - let(:event) { Fabricate(:relationship_severance_event) } - - describe '#import_from_active_follows!' do - before do - local_account.follow!(remote_account) - end - - it 'imports the follow relationships with the expected direction' do - event.import_from_active_follows!(local_account.active_relationships) - - relationships = event.severed_relationships.to_a - expect(relationships.size).to eq 1 - expect(relationships[0].account).to eq local_account - expect(relationships[0].target_account).to eq remote_account - end - end - - describe '#import_from_passive_follows!' do - before do - remote_account.follow!(local_account) - end - - it 'imports the follow relationships with the expected direction' do - event.import_from_passive_follows!(local_account.passive_relationships) - - relationships = event.severed_relationships.to_a - expect(relationships.size).to eq 1 - expect(relationships[0].account).to eq remote_account - expect(relationships[0].target_account).to eq local_account - end - end - - describe '#affected_local_accounts' do - before do - event.severed_relationships.create!(local_account: local_account, remote_account: remote_account, direction: :active) - end - - it 'correctly lists local accounts' do - expect(event.affected_local_accounts.to_a).to contain_exactly(local_account) - end - end -end diff --git a/spec/models/remote_follow_spec.rb b/spec/models/remote_follow_spec.rb deleted file mode 100644 index 81c726a40b..0000000000 --- a/spec/models/remote_follow_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe RemoteFollow do - before do - stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_return(request_fixture('webfinger.txt')) - end - - let(:attrs) { nil } - let(:remote_follow) { described_class.new(attrs) } - - describe '.initialize' do - subject { remote_follow.acct } - - context 'when attrs with acct' do - let(:attrs) { { acct: 'gargron@quitter.no' } } - - it 'returns acct' do - expect(subject).to eq 'gargron@quitter.no' - end - end - - context 'when attrs without acct' do - let(:attrs) { {} } - - it do - expect(subject).to be_nil - end - end - end - - describe '#valid?' do - subject { remote_follow.valid? } - - context 'when attrs with acct' do - let(:attrs) { { acct: 'gargron@quitter.no' } } - - it do - expect(subject).to be true - end - end - - context 'when attrs without acct' do - let(:attrs) { {} } - - it do - expect(subject).to be false - end - end - end - - describe '#subscribe_address_for' do - subject { remote_follow.subscribe_address_for(account) } - - before do - remote_follow.valid? - end - - let(:attrs) { { acct: 'gargron@quitter.no' } } - let(:account) { Fabricate(:account, username: 'alice') } - - it 'returns subscribe address' do - expect(subject).to eq 'https://quitter.no/main/ostatussub?profile=https%3A%2F%2Fcb6e6126.ngrok.io%2Fusers%2Falice' - end - end -end diff --git a/spec/models/report_filter_spec.rb b/spec/models/report_filter_spec.rb deleted file mode 100644 index 6baf0ea421..0000000000 --- a/spec/models/report_filter_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe ReportFilter do - describe 'with empty params' do - it 'defaults to unresolved reports list' do - filter = described_class.new({}) - - expect(filter.results).to eq Report.unresolved - end - end - - describe 'with invalid params' do - it 'raises with key error' do - filter = described_class.new(wrong: true) - - expect { filter.results }.to raise_error(/wrong/) - end - end - - describe 'with valid params' do - it 'combines filters on Report' do - filter = described_class.new(account_id: '123', resolved: true, target_account_id: '456') - - allow(Report).to receive_messages(where: Report.none, resolved: Report.none) - filter.results - expect(Report).to have_received(:where).with(account_id: '123') - expect(Report).to have_received(:where).with(target_account_id: '456') - expect(Report).to have_received(:resolved) - end - end -end diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb deleted file mode 100644 index d01d37bd8b..0000000000 --- a/spec/models/report_spec.rb +++ /dev/null @@ -1,154 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Report do - describe 'statuses' do - it 'returns the statuses for the report' do - status = Fabricate(:status) - _other = Fabricate(:status) - report = Fabricate(:report, status_ids: [status.id]) - - expect(report.statuses).to eq [status] - end - end - - describe 'media_attachments_count' do - it 'returns count of media attachments in statuses' do - status1 = Fabricate(:status, ordered_media_attachment_ids: [1, 2]) - status2 = Fabricate(:status, ordered_media_attachment_ids: [5]) - report = Fabricate(:report, status_ids: [status1.id, status2.id]) - - expect(report.media_attachments_count).to eq 3 - end - end - - describe 'assign_to_self!' do - subject { report.assigned_account_id } - - let(:report) { Fabricate(:report, assigned_account_id: original_account) } - let(:original_account) { Fabricate(:account) } - let(:current_account) { Fabricate(:account) } - - before do - report.assign_to_self!(current_account) - end - - it 'assigns to a given account' do - expect(subject).to eq current_account.id - end - end - - describe 'unassign!' do - subject { report.assigned_account_id } - - let(:report) { Fabricate(:report, assigned_account_id: account.id) } - let(:account) { Fabricate(:account) } - - before do - report.unassign! - end - - it 'unassigns' do - expect(subject).to be_nil - end - end - - describe 'resolve!' do - subject(:report) { Fabricate(:report, action_taken_at: nil, action_taken_by_account_id: nil) } - - let(:acting_account) { Fabricate(:account) } - - before do - report.resolve!(acting_account) - end - - it 'records action taken' do - expect(report.action_taken?).to be true - expect(report.action_taken_by_account_id).to eq acting_account.id - end - end - - describe 'unresolve!' do - subject(:report) { Fabricate(:report, action_taken_at: Time.now.utc, action_taken_by_account_id: acting_account.id) } - - let(:acting_account) { Fabricate(:account) } - - before do - report.unresolve! - end - - it 'unresolves' do - expect(report.action_taken?).to be false - expect(report.action_taken_by_account_id).to be_nil - end - end - - describe 'unresolved?' do - subject { report.unresolved? } - - let(:report) { Fabricate(:report, action_taken_at: action_taken) } - - context 'when action is taken' do - let(:action_taken) { Time.now.utc } - - it { is_expected.to be false } - end - - context 'when action not is taken' do - let(:action_taken) { nil } - - it { is_expected.to be true } - end - end - - describe 'history' do - subject(:action_logs) { report.history } - - let(:report) { Fabricate(:report, target_account_id: target_account.id, status_ids: [status.id], created_at: 3.days.ago, updated_at: 1.day.ago) } - let(:target_account) { Fabricate(:account) } - let(:status) { Fabricate(:status) } - - before do - Fabricate(:action_log, target_type: 'Report', account_id: target_account.id, target_id: report.id, created_at: 2.days.ago) - Fabricate(:action_log, target_type: 'Account', account_id: target_account.id, target_id: report.target_account_id, created_at: 2.days.ago) - Fabricate(:action_log, target_type: 'Status', account_id: target_account.id, target_id: status.id, created_at: 2.days.ago) - end - - it 'returns right logs' do - expect(action_logs.count).to eq 3 - end - end - - describe 'validations' do - let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } - - it 'is invalid if comment is longer than character limit and reporter is local' do - report = Fabricate.build(:report, comment: comment_over_limit) - expect(report.valid?).to be false - expect(report).to model_have_error_on_field(:comment) - end - - it 'is valid if comment is longer than character limit and reporter is not local' do - report = Fabricate.build(:report, account: remote_account, comment: comment_over_limit) - expect(report.valid?).to be true - end - - it 'is invalid if it references invalid rules' do - report = Fabricate.build(:report, category: :violation, rule_ids: [-1]) - expect(report.valid?).to be false - expect(report).to model_have_error_on_field(:rule_ids) - end - - it 'is invalid if it references rules but category is not "violation"' do - rule = Fabricate(:rule) - report = Fabricate.build(:report, category: :spam, rule_ids: rule.id) - expect(report.valid?).to be false - expect(report).to model_have_error_on_field(:rule_ids) - end - - def comment_over_limit - 'a' * described_class::COMMENT_SIZE_LIMIT * 2 - end - end -end diff --git a/spec/models/rule_spec.rb b/spec/models/rule_spec.rb deleted file mode 100644 index c9b9c55028..0000000000 --- a/spec/models/rule_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -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/scheduled_status_spec.rb b/spec/models/scheduled_status_spec.rb deleted file mode 100644 index 15031a5895..0000000000 --- a/spec/models/scheduled_status_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ScheduledStatus do - let(:account) { Fabricate(:account) } - - describe 'validations' do - context 'when scheduled_at is less than minimum offset' do - subject { Fabricate.build(:scheduled_status, scheduled_at: 4.minutes.from_now, account: account) } - - it 'is not valid', :aggregate_failures do - expect(subject).to_not be_valid - expect(subject.errors[:scheduled_at]).to include(I18n.t('scheduled_statuses.too_soon')) - end - end - - context 'when account has reached total limit' do - subject { Fabricate.build(:scheduled_status, account: account) } - - before do - allow(account.scheduled_statuses).to receive(:count).and_return(described_class::TOTAL_LIMIT) - end - - it 'is not valid', :aggregate_failures do - expect(subject).to_not be_valid - expect(subject.errors[:base]).to include(I18n.t('scheduled_statuses.over_total_limit', limit: ScheduledStatus::TOTAL_LIMIT)) - end - end - - context 'when account has reached daily limit' do - subject { Fabricate.build(:scheduled_status, account: account, scheduled_at: base_time + 10.minutes) } - - let(:base_time) { Time.current.change(hour: 12) } - - before do - stub_const('ScheduledStatus::DAILY_LIMIT', 3) - - travel_to base_time do - Fabricate.times(ScheduledStatus::DAILY_LIMIT, :scheduled_status, account: account, scheduled_at: base_time + 1.hour) - end - end - - it 'is not valid', :aggregate_failures do - expect(subject).to_not be_valid - expect(subject.errors[:base]).to include(I18n.t('scheduled_statuses.over_daily_limit', limit: ScheduledStatus::DAILY_LIMIT)) - end - end - end -end diff --git a/spec/models/session_activation_spec.rb b/spec/models/session_activation_spec.rb deleted file mode 100644 index bed411c369..0000000000 --- a/spec/models/session_activation_spec.rb +++ /dev/null @@ -1,141 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe SessionActivation do - describe '#detection' do - let(:session_activation) { Fabricate(:session_activation, user_agent: 'Chrome/62.0.3202.89') } - - it 'sets a Browser instance as detection' do - expect(session_activation.detection).to be_a Browser::Chrome - end - end - - describe '#browser' do - before do - allow(session_activation).to receive(:detection).and_return(detection) - end - - let(:detection) { instance_double(Browser::Chrome, id: 1) } - let(:session_activation) { Fabricate(:session_activation) } - - it 'returns detection.id' do - expect(session_activation.browser).to be 1 - end - end - - describe '#platform' do - before do - allow(session_activation).to receive(:detection).and_return(detection) - end - - let(:session_activation) { Fabricate(:session_activation) } - let(:detection) { instance_double(Browser::Chrome, platform: instance_double(Browser::Platform, id: 1)) } - - it 'returns detection.platform.id' do - expect(session_activation.platform).to be 1 - end - end - - describe '.active?' do - subject { described_class.active?(id) } - - context 'when id is absent' do - let(:id) { nil } - - it 'returns nil' do - expect(subject).to be_nil - end - end - - context 'when id is present' do - let(:id) { '1' } - let!(:session_activation) { Fabricate(:session_activation, session_id: id) } - - context 'when id exists as session_id' do - it 'returns true' do - expect(subject).to be true - end - end - - context 'when id does not exist as session_id' do - before do - session_activation.update!(session_id: '2') - end - - it 'returns false' do - expect(subject).to be false - end - end - end - end - - describe '.activate' do - let(:options) { { user: Fabricate(:user), session_id: '1' } } - - it 'calls create! and purge_old' do - allow(described_class).to receive(:create!).with(**options) - allow(described_class).to receive(:purge_old) - - described_class.activate(**options) - - expect(described_class).to have_received(:create!).with(**options) - expect(described_class).to have_received(:purge_old) - end - - it 'returns an instance of SessionActivation' do - expect(described_class.activate(**options)).to be_a described_class - end - end - - describe '.deactivate' do - context 'when id is absent' do - let(:id) { nil } - - it 'returns nil' do - expect(described_class.deactivate(id)).to be_nil - end - end - - context 'when id exists' do - let!(:session_activation) { Fabricate(:session_activation) } - - it 'destroys the record' do - described_class.deactivate(session_activation.session_id) - - expect { session_activation.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end - - describe '.purge_old' do - around do |example| - before = Rails.configuration.x.max_session_activations - Rails.configuration.x.max_session_activations = 1 - example.run - Rails.configuration.x.max_session_activations = before - end - - let!(:oldest_session_activation) { Fabricate(:session_activation, created_at: 10.days.ago) } - let!(:newest_session_activation) { Fabricate(:session_activation, created_at: 5.days.ago) } - - it 'preserves the newest X records based on config' do - described_class.purge_old - - expect { oldest_session_activation.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { newest_session_activation.reload }.to_not raise_error - end - end - - describe '.exclusive' do - let!(:unwanted_session_activation) { Fabricate(:session_activation) } - let!(:wanted_session_activation) { Fabricate(:session_activation) } - - it 'preserves supplied record and destroys all others' do - described_class.exclusive(wanted_session_activation.session_id) - - expect { unwanted_session_activation.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { wanted_session_activation.reload }.to_not raise_error - end - end -end diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb deleted file mode 100644 index a1e24e8350..0000000000 --- a/spec/models/setting_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Setting do - describe '#to_param' do - let(:setting) { Fabricate(:setting, var: var) } - let(:var) { 'var' } - - it 'returns setting.var' do - expect(setting.to_param).to eq var - end - end - - describe '.[]' do - let(:key) { 'key' } - let(:cache_key) { 'cache-key' } - let(:cache_value) { 'cache-value' } - - before do - allow(described_class).to receive(:cache_key).with(key).and_return(cache_key) - end - - context 'when Rails.cache does not exists' do - before do - allow(described_class).to receive(:default_settings).and_return(default_settings) - - Fabricate(:setting, var: key, value: 42) if save_setting - - Rails.cache.delete(cache_key) - end - - let(:default_value) { 'default_value' } - let(:default_settings) { { key => default_value } } - let(:save_setting) { true } - - context 'when the setting has been saved to database' do - it 'returns the value from database' do - callback = double - allow(callback).to receive(:call) - - ActiveSupport::Notifications.subscribed callback, 'sql.active_record' do - expect(described_class[key]).to eq 42 - end - - expect(callback).to have_received(:call) - end - end - - context 'when the setting has not been saved to database' do - let(:save_setting) { false } - - it 'returns default_settings[key]' do - expect(described_class[key]).to be default_settings[key] - end - end - end - - context 'when Rails.cache exists' do - before do - Rails.cache.write(cache_key, cache_value) - end - - it 'does not query the database' do - callback = double - allow(callback).to receive(:call) - ActiveSupport::Notifications.subscribed callback, 'sql.active_record' do - described_class[key] - end - expect(callback).to_not have_received(:call) - end - - it 'returns the cached value' do - expect(described_class[key]).to eq cache_value - end - end - end -end diff --git a/spec/models/severed_relationship_spec.rb b/spec/models/severed_relationship_spec.rb deleted file mode 100644 index 0f922d7983..0000000000 --- a/spec/models/severed_relationship_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe SeveredRelationship do - let(:local_account) { Fabricate(:account) } - let(:remote_account) { Fabricate(:account, domain: 'example.com') } - let(:event) { Fabricate(:relationship_severance_event) } - - describe '#account' do - context 'when the local account is the follower' do - let(:severed_relationship) { Fabricate(:severed_relationship, relationship_severance_event: event, local_account: local_account, remote_account: remote_account, direction: :active) } - - it 'returns the local account' do - expect(severed_relationship.account).to eq local_account - end - end - - context 'when the local account is being followed' do - let(:severed_relationship) { Fabricate(:severed_relationship, relationship_severance_event: event, local_account: local_account, remote_account: remote_account, direction: :passive) } - - it 'returns the remote account' do - expect(severed_relationship.account).to eq remote_account - end - end - end - - describe '#target_account' do - context 'when the local account is the follower' do - let(:severed_relationship) { Fabricate(:severed_relationship, relationship_severance_event: event, local_account: local_account, remote_account: remote_account, direction: :active) } - - it 'returns the remote account' do - expect(severed_relationship.target_account).to eq remote_account - end - end - - context 'when the local account is being followed' do - let(:severed_relationship) { Fabricate(:severed_relationship, relationship_severance_event: event, local_account: local_account, remote_account: remote_account, direction: :passive) } - - it 'returns the local account' do - expect(severed_relationship.target_account).to eq local_account - end - end - end -end diff --git a/spec/models/site_upload_spec.rb b/spec/models/site_upload_spec.rb deleted file mode 100644 index 9689bce9ee..0000000000 --- a/spec/models/site_upload_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe SiteUpload do - describe '#cache_key' do - let(:site_upload) { described_class.new(var: 'var') } - - it 'returns cache_key' do - expect(site_upload.cache_key).to eq 'site_uploads/var' - end - end -end diff --git a/spec/models/software_update_spec.rb b/spec/models/software_update_spec.rb deleted file mode 100644 index 0a494b0c4c..0000000000 --- a/spec/models/software_update_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe SoftwareUpdate do - describe '.pending_to_a' do - before do - allow(Mastodon::Version).to receive(:gem_version).and_return(Gem::Version.new(mastodon_version)) - - Fabricate(:software_update, version: '3.4.42', type: 'patch', urgent: true) - Fabricate(:software_update, version: '3.5.0', type: 'minor', urgent: false) - Fabricate(:software_update, version: '4.2.0', type: 'major', urgent: false) - end - - context 'when the Mastodon version is an outdated release' do - let(:mastodon_version) { '3.4.0' } - - it 'returns the expected versions' do - expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('3.4.42', '3.5.0', '4.2.0') - end - end - - context 'when the Mastodon version is more recent than anything last returned by the server' do - let(:mastodon_version) { '5.0.0' } - - it 'returns the expected versions' do - expect(described_class.pending_to_a.pluck(:version)).to eq [] - end - end - - context 'when the Mastodon version is an outdated nightly' do - let(:mastodon_version) { '4.3.0-nightly.2023-09-10' } - - before do - Fabricate(:software_update, version: '4.3.0-nightly.2023-09-12', type: 'major', urgent: true) - end - - it 'returns the expected versions' do - expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-nightly.2023-09-12') - end - end - - context 'when the Mastodon version is a very outdated nightly' do - let(:mastodon_version) { '4.2.0-nightly.2023-07-10' } - - it 'returns the expected versions' do - expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.2.0') - end - end - - context 'when the Mastodon version is an outdated dev version' do - let(:mastodon_version) { '4.3.0-0.dev.0' } - - before do - Fabricate(:software_update, version: '4.3.0-0.dev.2', type: 'major', urgent: true) - end - - it 'returns the expected versions' do - expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-0.dev.2') - end - end - - context 'when the Mastodon version is an outdated beta version' do - let(:mastodon_version) { '4.3.0-beta1' } - - before do - Fabricate(:software_update, version: '4.3.0-beta2', type: 'major', urgent: true) - end - - it 'returns the expected versions' do - expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-beta2') - end - end - - context 'when the Mastodon version is an outdated beta version and there is a rc' do - let(:mastodon_version) { '4.3.0-beta1' } - - before do - Fabricate(:software_update, version: '4.3.0-rc1', type: 'major', urgent: true) - end - - it 'returns the expected versions' do - expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-rc1') - end - end - end -end diff --git a/spec/models/status_edit_spec.rb b/spec/models/status_edit_spec.rb deleted file mode 100644 index 2d33514522..0000000000 --- a/spec/models/status_edit_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -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_pin_spec.rb b/spec/models/status_pin_spec.rb deleted file mode 100644 index da375009ae..0000000000 --- a/spec/models/status_pin_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe StatusPin do - describe 'validations' do - it 'allows pins of own statuses' do - account = Fabricate(:account) - status = Fabricate(:status, account: account) - - expect(described_class.new(account: account, status: status).save).to be true - end - - it 'does not allow pins of statuses by someone else' do - account = Fabricate(:account) - status = Fabricate(:status) - - expect(described_class.new(account: account, status: status).save).to be false - end - - it 'does not allow pins of reblogs' do - account = Fabricate(:account) - status = Fabricate(:status, account: account) - reblog = Fabricate(:status, reblog: status) - - expect(described_class.new(account: account, status: reblog).save).to be false - end - - it 'does allow pins of direct statuses' do - account = Fabricate(:account) - status = Fabricate(:status, account: account, visibility: :private) - - expect(described_class.new(account: account, status: status).save).to be true - end - - it 'does not allow pins of direct statuses' do - account = Fabricate(:account) - status = Fabricate(:status, account: account, visibility: :direct) - - expect(described_class.new(account: account, status: status).save).to be false - end - - context 'with a pin limit' do - before { stub_const('StatusPinValidator::PIN_LIMIT', 2) } - - it 'does not allow pins above the max' do - account = Fabricate(:account) - - Fabricate.times(StatusPinValidator::PIN_LIMIT, :status_pin, account: account) - - pin = described_class.new(account: account, status: Fabricate(:status, account: account)) - expect(pin.save) - .to be(false) - - expect(pin.errors[:base]) - .to contain_exactly(I18n.t('statuses.pin_errors.limit')) - end - - it 'allows pins above the max for remote accounts' do - account = Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') - - Fabricate.times(StatusPinValidator::PIN_LIMIT, :status_pin, account: account) - - pin = described_class.new(account: account, status: Fabricate(:status, account: account)) - expect(pin.save) - .to be(true) - - expect(pin.errors[:base]) - .to be_empty - end - end - end -end diff --git a/spec/models/status_reaction_spec.rb b/spec/models/status_reaction_spec.rb deleted file mode 100644 index ccfa9ee8d8..0000000000 --- a/spec/models/status_reaction_spec.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb deleted file mode 100644 index c0b0c2420f..0000000000 --- a/spec/models/status_spec.rb +++ /dev/null @@ -1,577 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Status do - subject { Fabricate(:status, account: alice) } - - let(:alice) { Fabricate(:account, username: 'alice') } - let(:bob) { Fabricate(:account, username: 'bob') } - let(:other) { Fabricate(:status, account: bob, text: 'Skulls for the skull god! The enemy\'s gates are sideways!') } - - describe '#local?' do - it 'returns true when no remote URI is set' do - expect(subject.local?).to be true - end - - it 'returns false if a remote URI is set' do - alice.update(domain: 'example.com') - subject.save - expect(subject.local?).to be false - end - - it 'returns true if a URI is set and `local` is true' do - subject.update(uri: 'example.com', local: true) - expect(subject.local?).to be true - end - end - - describe '#reblog?' do - it 'returns true when the status reblogs another status' do - subject.reblog = other - expect(subject.reblog?).to be true - end - - it 'returns false if the status is self-contained' do - expect(subject.reblog?).to be false - end - end - - describe '#reply?' do - it 'returns true if the status references another' do - subject.thread = other - expect(subject.reply?).to be true - end - - it 'returns false if the status is self-contained' do - expect(subject.reply?).to be false - end - end - - describe '#verb' do - context 'when destroyed?' do - it 'returns :delete' do - subject.destroy! - expect(subject.verb).to be :delete - end - end - - context 'when not destroyed?' do - context 'when reblog?' do - it 'returns :share' do - subject.reblog = other - expect(subject.verb).to be :share - end - end - - context 'when not reblog?' do - it 'returns :post' do - subject.reblog = nil - expect(subject.verb).to be :post - end - end - end - end - - describe '#object_type' do - it 'is note when the status is self-contained' do - expect(subject.object_type).to be :note - end - - it 'is comment when the status replies to another' do - subject.thread = other - expect(subject.object_type).to be :comment - end - end - - describe '#hidden?' do - context 'when private_visibility?' do - it 'returns true' do - subject.visibility = :private - expect(subject.hidden?).to be true - end - end - - context 'when direct_visibility?' do - it 'returns true' do - subject.visibility = :direct - expect(subject.hidden?).to be true - end - end - - context 'when public_visibility?' do - it 'returns false' do - subject.visibility = :public - expect(subject.hidden?).to be false - end - end - - context 'when unlisted_visibility?' do - it 'returns false' do - subject.visibility = :unlisted - expect(subject.hidden?).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 - end - - it 'returns the text of the reblogged status' do - subject.reblog = other - expect(subject.content).to eql other.text - end - end - - describe '#target' do - it 'returns nil if the status is self-contained' do - expect(subject.target).to be_nil - end - - it 'returns nil if the status is a reply' do - subject.thread = other - expect(subject.target).to be_nil - end - - it 'returns the reblogged status' do - subject.reblog = other - expect(subject.target).to eq other - end - end - - describe '#reblogs_count' do - it 'is the number of reblogs' do - Fabricate(:status, account: bob, reblog: subject) - Fabricate(:status, account: alice, reblog: subject) - - expect(subject.reblogs_count).to eq 2 - end - - it 'is decremented when reblog is removed' do - reblog = Fabricate(:status, account: bob, reblog: subject) - expect(subject.reblogs_count).to eq 1 - reblog.destroy - expect(subject.reblogs_count).to eq 0 - end - - it 'does not fail when original is deleted before reblog' do - reblog = Fabricate(:status, account: bob, reblog: subject) - expect(subject.reblogs_count).to eq 1 - expect { subject.destroy }.to_not raise_error - expect(described_class.find_by(id: reblog.id)).to be_nil - end - end - - describe '#replies_count' do - it 'is the number of replies' do - Fabricate(:status, account: bob, thread: subject) - expect(subject.replies_count).to eq 1 - end - - it 'is decremented when reply is removed' do - reply = Fabricate(:status, account: bob, thread: subject) - expect(subject.replies_count).to eq 1 - reply.destroy - expect(subject.replies_count).to eq 0 - end - end - - describe '#favourites_count' do - it 'is the number of favorites' do - Fabricate(:favourite, account: bob, status: subject) - Fabricate(:favourite, account: alice, status: subject) - - expect(subject.favourites_count).to eq 2 - end - - it 'is decremented when favourite is removed' do - favourite = Fabricate(:favourite, account: bob, status: subject) - expect(subject.favourites_count).to eq 1 - favourite.destroy - expect(subject.favourites_count).to eq 0 - end - end - - describe '#proper' do - it 'is itself for original statuses' do - expect(subject.proper).to eq subject - end - - it 'is the source status for reblogs' do - subject.reblog = other - expect(subject.proper).to eq other - end - end - - describe 'on create' do - subject { described_class.new } - - let(:local_account) { Fabricate(:account, username: 'local', domain: nil) } - let(:remote_account) { Fabricate(:account, username: 'remote', domain: 'example.com') } - - describe 'on a status that ends with the local-only emoji' do - before do - subject.text = "A toot #{subject.local_only_emoji}" - end - - context 'when the status originates from this instance' do - before do - subject.account = local_account - end - - it 'is marked local-only' do - subject.save! - - expect(subject).to be_local_only - end - end - - context 'when the status is remote' do - before do - subject.account = remote_account - end - - it 'is not marked local-only' do - subject.save! - - expect(subject).to_not be_local_only - end - end - end - end - - describe '#reported?' do - context 'when the status is not reported' do - it 'returns false' do - expect(subject.reported?).to be false - end - end - - context 'when the status is part of an open report' do - before do - Fabricate(:report, target_account: subject.account, status_ids: [subject.id]) - end - - it 'returns true' do - expect(subject.reported?).to be true - end - end - - context 'when the status is part of a closed report with an account warning mentioning the account' do - before do - report = Fabricate(:report, target_account: subject.account, status_ids: [subject.id]) - report.resolve!(Fabricate(:account)) - Fabricate(:account_warning, target_account: subject.account, status_ids: [subject.id], report: report) - end - - it 'returns true' do - expect(subject.reported?).to be true - end - end - - context 'when the status is part of a closed report with an account warning not mentioning the account' do - before do - report = Fabricate(:report, target_account: subject.account, status_ids: [subject.id]) - report.resolve!(Fabricate(:account)) - Fabricate(:account_warning, target_account: subject.account, report: report) - end - - it 'returns false' do - expect(subject.reported?).to be false - end - end - end - - describe '#ordered_media_attachments' do - let(:status) { Fabricate(:status) } - - let(:first_attachment) { Fabricate(:media_attachment) } - let(:second_attachment) { Fabricate(:media_attachment) } - let(:last_attachment) { Fabricate(:media_attachment) } - let(:extra_attachment) { Fabricate(:media_attachment) } - - before do - stub_const('Status::MEDIA_ATTACHMENTS_LIMIT', 3) - - # Add attachments out of order - status.media_attachments << second_attachment - status.media_attachments << last_attachment - status.media_attachments << extra_attachment - status.media_attachments << first_attachment - end - - context 'when ordered_media_attachment_ids is not set' do - it 'returns up to MEDIA_ATTACHMENTS_LIMIT attachments' do - expect(status.ordered_media_attachments.size).to eq Status::MEDIA_ATTACHMENTS_LIMIT - end - end - - context 'when ordered_media_attachment_ids is set' do - before do - status.update!(ordered_media_attachment_ids: [first_attachment.id, second_attachment.id, last_attachment.id, extra_attachment.id]) - end - - it 'returns up to MEDIA_ATTACHMENTS_LIMIT attachments in the expected order' do - expect(status.ordered_media_attachments).to eq [first_attachment, second_attachment, last_attachment] - end - end - end - - describe '.mutes_map' do - subject { described_class.mutes_map([status.conversation.id], account) } - - let(:status) { Fabricate(:status) } - let(:account) { Fabricate(:account) } - - it 'returns a hash' do - expect(subject).to be_a Hash - end - - it 'contains true value' do - account.mute_conversation!(status.conversation) - expect(subject[status.conversation.id]).to be true - end - end - - describe '.favourites_map' do - subject { described_class.favourites_map([status], account) } - - let(:status) { Fabricate(:status) } - let(:account) { Fabricate(:account) } - - it 'returns a hash' do - expect(subject).to be_a Hash - end - - it 'contains true value' do - Fabricate(:favourite, status: status, account: account) - expect(subject[status.id]).to be true - end - end - - describe '.reblogs_map' do - subject { described_class.reblogs_map([status], account) } - - let(:status) { Fabricate(:status) } - let(:account) { Fabricate(:account) } - - it 'returns a hash' do - expect(subject).to be_a Hash - end - - it 'contains true value' do - Fabricate(:status, account: account, reblog: status) - expect(subject[status.id]).to be true - end - end - - describe '.as_direct_timeline' do - subject(:results) { described_class.as_direct_timeline(account) } - - let(:account) { Fabricate(:account) } - let(:followed) { Fabricate(:account) } - let(:not_followed) { Fabricate(:account) } - - let!(:self_public_status) { Fabricate(:status, account: account, visibility: :public) } - let!(:self_direct_status) { Fabricate(:status, account: account, visibility: :direct) } - let!(:followed_public_status) { Fabricate(:status, account: followed, visibility: :public) } - let!(:followed_direct_status) { Fabricate(:status, account: followed, visibility: :direct) } - let!(:not_followed_direct_status) { Fabricate(:status, account: not_followed, visibility: :direct) } - - before do - account.follow!(followed) - end - - it 'does not include public statuses from self' do - expect(results).to_not include(self_public_status) - end - - it 'includes direct statuses from self' do - expect(results).to include(self_direct_status) - end - - it 'does not include public statuses from followed' do - expect(results).to_not include(followed_public_status) - end - - it 'does not include direct statuses not mentioning recipient from followed' do - expect(results).to_not include(followed_direct_status) - end - - it 'does not include direct statuses not mentioning recipient from non-followed' do - expect(results).to_not include(not_followed_direct_status) - end - - it 'includes direct statuses mentioning recipient from followed' do - Fabricate(:mention, account: account, status: followed_direct_status) - results2 = described_class.as_direct_timeline(account) - expect(results2).to include(followed_direct_status) - end - - it 'includes direct statuses mentioning recipient from non-followed' do - Fabricate(:mention, account: account, status: not_followed_direct_status) - results2 = described_class.as_direct_timeline(account) - expect(results2).to include(not_followed_direct_status) - end - end - - describe '.tagged_with' do - let(:tag_cats) { Fabricate(:tag, name: 'cats') } - let(:tag_dogs) { Fabricate(:tag, name: 'dogs') } - let(:tag_zebras) { Fabricate(:tag, name: 'zebras') } - let!(:status_with_tag_cats) { Fabricate(:status, tags: [tag_cats]) } - let!(:status_with_tag_dogs) { Fabricate(:status, tags: [tag_dogs]) } - let!(:status_tagged_with_zebras) { Fabricate(:status, tags: [tag_zebras]) } - let!(:status_without_tags) { Fabricate(:status, tags: []) } - let!(:status_with_all_tags) { Fabricate(:status, tags: [tag_cats, tag_dogs, tag_zebras]) } - - context 'when given one tag' do - it 'returns the expected statuses' do - expect(described_class.tagged_with([tag_cats.id])) - .to include(status_with_tag_cats, status_with_all_tags) - .and not_include(status_without_tags) - expect(described_class.tagged_with([tag_dogs.id])) - .to include(status_with_tag_dogs, status_with_all_tags) - .and not_include(status_without_tags) - expect(described_class.tagged_with([tag_zebras.id])) - .to include(status_tagged_with_zebras, status_with_all_tags) - .and not_include(status_without_tags) - end - end - - context 'when given multiple tags' do - it 'returns the expected statuses' do - expect(described_class.tagged_with([tag_cats.id, tag_dogs.id])) - .to include(status_with_tag_cats, status_with_tag_dogs, status_with_all_tags) - .and not_include(status_without_tags) - expect(described_class.tagged_with([tag_cats.id, tag_zebras.id])) - .to include(status_with_tag_cats, status_tagged_with_zebras, status_with_all_tags) - .and not_include(status_without_tags) - expect(described_class.tagged_with([tag_dogs.id, tag_zebras.id])) - .to include(status_with_tag_dogs, status_tagged_with_zebras, status_with_all_tags) - .and not_include(status_without_tags) - end - end - end - - describe '.tagged_with_all' do - let(:tag_cats) { Fabricate(:tag, name: 'cats') } - let(:tag_dogs) { Fabricate(:tag, name: 'dogs') } - let(:tag_zebras) { Fabricate(:tag, name: 'zebras') } - let!(:status_with_tag_cats) { Fabricate(:status, tags: [tag_cats]) } - let!(:status_with_tag_dogs) { Fabricate(:status, tags: [tag_dogs]) } - let!(:status_tagged_with_zebras) { Fabricate(:status, tags: [tag_zebras]) } - let!(:status_without_tags) { Fabricate(:status, tags: []) } - let!(:status_with_all_tags) { Fabricate(:status, tags: [tag_cats, tag_dogs]) } - - context 'when given one tag' do - it 'returns the expected statuses' do - expect(described_class.tagged_with_all([tag_cats.id])) - .to include(status_with_tag_cats, status_with_all_tags) - .and not_include(status_without_tags) - expect(described_class.tagged_with_all([tag_dogs.id])) - .to include(status_with_tag_dogs, status_with_all_tags) - .and not_include(status_without_tags) - expect(described_class.tagged_with_all([tag_zebras.id])) - .to include(status_tagged_with_zebras) - .and not_include(status_without_tags) - end - end - - context 'when given multiple tags' do - it 'returns the expected statuses' do - expect(described_class.tagged_with_all([tag_cats.id, tag_dogs.id])) - .to include(status_with_all_tags) - expect(described_class.tagged_with_all([tag_cats.id, tag_zebras.id])) - .to eq [] - expect(described_class.tagged_with_all([tag_dogs.id, tag_zebras.id])) - .to eq [] - end - end - end - - describe '.tagged_with_none' do - let(:tag_cats) { Fabricate(:tag, name: 'cats') } - let(:tag_dogs) { Fabricate(:tag, name: 'dogs') } - let(:tag_zebras) { Fabricate(:tag, name: 'zebras') } - let!(:status_with_tag_cats) { Fabricate(:status, tags: [tag_cats]) } - let!(:status_with_tag_dogs) { Fabricate(:status, tags: [tag_dogs]) } - let!(:status_tagged_with_zebras) { Fabricate(:status, tags: [tag_zebras]) } - let!(:status_without_tags) { Fabricate(:status, tags: []) } - let!(:status_with_all_tags) { Fabricate(:status, tags: [tag_cats, tag_dogs, tag_zebras]) } - - context 'when given one tag' do - it 'returns the expected statuses' do - expect(described_class.tagged_with_none([tag_cats.id])) - .to include(status_with_tag_dogs, status_tagged_with_zebras, status_without_tags) - .and not_include(status_with_all_tags) - expect(described_class.tagged_with_none([tag_dogs.id])) - .to include(status_with_tag_cats, status_tagged_with_zebras, status_without_tags) - .and not_include(status_with_all_tags) - expect(described_class.tagged_with_none([tag_zebras.id])) - .to include(status_with_tag_cats, status_with_tag_dogs, status_without_tags) - .and not_include(status_with_all_tags) - end - end - - context 'when given multiple tags' do - it 'returns the expected statuses' do - expect(described_class.tagged_with_none([tag_cats.id, tag_dogs.id])) - .to include(status_tagged_with_zebras, status_without_tags) - .and not_include(status_with_all_tags) - expect(described_class.tagged_with_none([tag_cats.id, tag_zebras.id])) - .to include(status_with_tag_dogs, status_without_tags) - .and not_include(status_with_all_tags) - expect(described_class.tagged_with_none([tag_dogs.id, tag_zebras.id])) - .to include(status_with_tag_cats, status_without_tags) - .and not_include(status_with_all_tags) - end - end - end - - describe 'before_validation' do - it 'sets account being replied to correctly over intermediary nodes' do - first_status = Fabricate(:status, account: bob) - intermediary = Fabricate(:status, thread: first_status, account: alice) - final = Fabricate(:status, thread: intermediary, account: alice) - - expect(final.in_reply_to_account_id).to eq bob.id - end - - it 'creates new conversation for stand-alone status' do - expect(described_class.create(account: alice, text: 'First').conversation_id).to_not be_nil - end - - it 'keeps conversation of parent node' do - parent = Fabricate(:status, text: 'First') - expect(described_class.create(account: alice, thread: parent, text: 'Response').conversation_id).to eq parent.conversation_id - end - - it 'sets `local` to true for status by local account' do - expect(described_class.create(account: alice, text: 'foo').local).to be true - end - - it 'sets `local` to false for status by remote account' do - alice.update(domain: 'example.com') - expect(described_class.create(account: alice, text: 'foo').local).to be false - end - end - - describe 'validation' do - it 'disallow empty uri for remote status' do - alice.update(domain: 'example.com') - status = Fabricate.build(:status, uri: '', account: alice) - expect(status).to model_have_error_on_field(:uri) - end - end - - describe 'after_create' do - it 'saves ActivityPub uri as uri for local status' do - status = described_class.create(account: alice, text: 'foo') - status.reload - expect(status.uri).to start_with('https://') - end - end -end diff --git a/spec/models/tag_feed_spec.rb b/spec/models/tag_feed_spec.rb deleted file mode 100644 index 82d5af0f02..0000000000 --- a/spec/models/tag_feed_spec.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe TagFeed do - describe '#get' do - let(:account) { Fabricate(:account) } - let(:tag_cats) { Fabricate(:tag, name: 'cats') } - let(:tag_dogs) { Fabricate(:tag, name: 'dogs') } - let!(:status_tagged_with_cats) { Fabricate(:status, tags: [tag_cats]) } - let!(:status_tagged_with_dogs) { Fabricate(:status, tags: [tag_dogs]) } - let!(:both) { Fabricate(:status, tags: [tag_cats, tag_dogs]) } - - it 'can add tags in "any" mode' do - results = described_class.new(tag_cats, nil, any: [tag_dogs.name]).get(20) - expect(results).to include status_tagged_with_cats - expect(results).to include status_tagged_with_dogs - expect(results).to include both - end - - it 'can remove tags in "all" mode' do - results = described_class.new(tag_cats, nil, all: [tag_dogs.name]).get(20) - expect(results).to_not include status_tagged_with_cats - expect(results).to_not include status_tagged_with_dogs - expect(results).to include both - end - - it 'can remove tags in "none" mode' do - results = described_class.new(tag_cats, nil, none: [tag_dogs.name]).get(20) - expect(results).to include status_tagged_with_cats - expect(results).to_not include status_tagged_with_dogs - expect(results).to_not include both - end - - it 'ignores an invalid mode' do - results = described_class.new(tag_cats, nil, wark: [tag_dogs.name]).get(20) - expect(results).to include status_tagged_with_cats - expect(results).to_not include status_tagged_with_dogs - expect(results).to include both - end - - it 'handles being passed non existent tag names' do - results = described_class.new(tag_cats, nil, any: ['wark']).get(20) - expect(results).to include status_tagged_with_cats - expect(results).to_not include status_tagged_with_dogs - expect(results).to include both - end - - it 'can restrict to an account' do - BlockService.new.call(account, status_tagged_with_cats.account) - results = described_class.new(tag_cats, account, none: [tag_dogs.name]).get(20) - expect(results).to_not include status_tagged_with_cats - end - - it 'can restrict to local' do - status_tagged_with_cats.account.update(domain: 'example.com') - status_tagged_with_cats.update(local: false, uri: 'example.com/toot') - results = described_class.new(tag_cats, nil, any: [tag_dogs.name], local: true).get(20) - expect(results).to_not include status_tagged_with_cats - end - - it 'allows replies to be included' do - original = Fabricate(:status) - status = Fabricate(:status, tags: [tag_cats], in_reply_to_id: original.id) - - results = described_class.new(tag_cats, nil).get(20) - expect(results).to include(status) - end - - context 'when the feed contains a local-only status' do - let!(:status) { Fabricate(:status, tags: [tag_cats], local_only: true) } - - it 'does not show local-only statuses without a viewer' do - results = described_class.new(tag_cats, nil).get(20) - expect(results).to_not include(status) - end - - it 'shows local-only statuses given a viewer' do - results = described_class.new(tag_cats, account).get(20) - expect(results).to include(status) - end - end - end -end diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb deleted file mode 100644 index ff0a055113..0000000000 --- a/spec/models/tag_spec.rb +++ /dev/null @@ -1,274 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Tag do - describe 'validations' do - it 'invalid with #' do - expect(described_class.new(name: '#hello_world')).to_not be_valid - end - - it 'invalid with .' do - expect(described_class.new(name: '.abcdef123')).to_not be_valid - end - - it 'invalid with spaces' do - expect(described_class.new(name: 'hello world')).to_not be_valid - end - - it 'valid with aesthetic' do - expect(described_class.new(name: 'aesthetic')).to be_valid - end - end - - describe 'HASHTAG_RE' do - subject { described_class::HASHTAG_RE } - - it 'does not match URLs with anchors with non-hashtag characters' do - expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil - end - - it 'does not match URLs with hashtag-like anchors' do - expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit')).to be_nil - end - - it 'does not match URLs with hashtag-like anchors after a numeral' do - expect(subject.match('https://gcc.gnu.org/bugzilla/show_bug.cgi?id=111895#c4')).to be_nil - end - - it 'does not match URLs with hashtag-like anchors after a non-ascii character' do - expect(subject.match('https://example.org/testé#foo')).to be_nil - end - - it 'does not match URLs with hashtag-like anchors after an empty query parameter' do - expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)?foo=#Lawsuit')).to be_nil - end - - it 'matches #aesthetic' do - expect(subject.match('this is #aesthetic').to_s).to eq '#aesthetic' - end - - it 'matches digits at the start' do - expect(subject.match('hello #3d').to_s).to eq '#3d' - end - - it 'matches digits in the middle' do - expect(subject.match('hello #l33ts35k').to_s).to eq '#l33ts35k' - end - - it 'matches digits at the end' do - expect(subject.match('hello #world2016').to_s).to eq '#world2016' - end - - it 'matches underscores at the beginning' do - expect(subject.match('hello #_test').to_s).to eq '#_test' - end - - it 'matches underscores at the end' do - expect(subject.match('hello #test_').to_s).to eq '#test_' - end - - it 'matches underscores in the middle' do - expect(subject.match('hello #one_two_three').to_s).to eq '#one_two_three' - end - - it 'matches middle dots' do - expect(subject.match('hello #one·two·three').to_s).to eq '#one·two·three' - end - - it 'matches ・unicode in ぼっち・ざ・ろっく correctly' do - expect(subject.match('testing #ぼっち・ざ・ろっく').to_s).to eq '#ぼっち・ざ・ろっく' - end - - it 'matches ZWNJ' do - expect(subject.match('just add #نرم‌افزار and').to_s).to eq '#نرم‌افزار' - end - - it 'does not match middle dots at the start' do - expect(subject.match('hello #·one·two·three')).to be_nil - end - - it 'does not match middle dots at the end' do - expect(subject.match('hello #one·two·three·').to_s).to eq '#one·two·three' - end - - it 'does not match purely-numeric hashtags' do - expect(subject.match('hello #0123456')).to be_nil - end - - it 'matches hashtags immediately following the letter ß' do - expect(subject.match('Hello toß #ruby').to_s).to eq '#ruby' - end - - it 'matches hashtags containing uppercase characters' do - expect(subject.match('Hello #rubyOnRails').to_s).to eq '#rubyOnRails' - end - end - - describe '#to_param' do - it 'returns name' do - tag = Fabricate(:tag, name: 'foo') - expect(tag.to_param).to eq 'foo' - end - end - - describe '#formatted_name' do - it 'returns name with a proceeding hash symbol' do - tag = Fabricate(:tag, name: 'foo') - expect(tag.formatted_name).to eq '#foo' - end - - it 'returns display_name with a proceeding hash symbol, if display name present' do - tag = Fabricate(:tag, name: 'foobar', display_name: 'FooBar') - expect(tag.formatted_name).to eq '#FooBar' - end - end - - describe '.recently_used' do - let(:account) { Fabricate(:account) } - let(:other_person_status) { Fabricate(:status) } - let(:out_of_range) { Fabricate(:status, account: account) } - let(:older_in_range) { Fabricate(:status, account: account) } - let(:newer_in_range) { Fabricate(:status, account: account) } - let(:unused_tag) { Fabricate(:tag) } - let(:used_tag_one) { Fabricate(:tag) } - let(:used_tag_two) { Fabricate(:tag) } - let(:used_tag_on_out_of_range) { Fabricate(:tag) } - - before do - stub_const 'Tag::RECENT_STATUS_LIMIT', 2 - - other_person_status.tags << used_tag_one - - out_of_range.tags << used_tag_on_out_of_range - - older_in_range.tags << used_tag_one - older_in_range.tags << used_tag_two - - newer_in_range.tags << used_tag_one - end - - it 'returns tags used by account within last X statuses ordered most used first' do - results = described_class.recently_used(account) - - expect(results) - .to eq([used_tag_one, used_tag_two]) - end - end - - describe '.find_normalized' do - it 'returns tag for a multibyte case-insensitive name' do - upcase_string = 'abcABCabcABCやゆよ' - downcase_string = 'abcabcabcabcやゆよ' - - tag = Fabricate(:tag, name: HashtagNormalizer.new.normalize(downcase_string)) - expect(described_class.find_normalized(upcase_string)).to eq tag - end - end - - describe '.not_featured_by' do - let!(:account) { Fabricate(:account) } - let!(:fun) { Fabricate(:tag, name: 'fun') } - let!(:games) { Fabricate(:tag, name: 'games') } - - before do - Fabricate :featured_tag, account: account, name: 'games' - Fabricate :featured_tag, name: 'fun' - end - - it 'returns tags not featured by the account' do - results = described_class.not_featured_by(account) - - expect(results) - .to include(fun) - .and not_include(games) - end - end - - describe '.matches_name' do - it 'returns tags for multibyte case-insensitive names' do - upcase_string = 'abcABCabcABCやゆよ' - downcase_string = 'abcabcabcabcやゆよ' - - tag = Fabricate(:tag, name: HashtagNormalizer.new.normalize(downcase_string)) - expect(described_class.matches_name(upcase_string)).to eq [tag] - end - - it 'uses the LIKE operator' do - result = %q[SELECT "tags".* FROM "tags" WHERE LOWER("tags"."name") LIKE LOWER('100abc%')] - expect(described_class.matches_name('100%abc').to_sql).to eq result - end - end - - describe '.matching_name' do - it 'returns tags for multibyte case-insensitive names' do - upcase_string = 'abcABCabcABCやゆよ' - downcase_string = 'abcabcabcabcやゆよ' - - tag = Fabricate(:tag, name: HashtagNormalizer.new.normalize(downcase_string)) - expect(described_class.matching_name(upcase_string)).to eq [tag] - end - end - - describe '.find_or_create_by_names' do - let(:upcase_string) { 'abcABCabcABCやゆよ' } - let(:downcase_string) { 'abcabcabcabcやゆよ' } - - it 'runs a passed block once per tag regardless of duplicates' do - count = 0 - - described_class.find_or_create_by_names([upcase_string, downcase_string]) do |_tag| - count += 1 - end - - expect(count).to eq 1 - end - end - - describe '.search_for' do - it 'finds tag records with matching names' do - tag = Fabricate(:tag, name: 'match') - _miss_tag = Fabricate(:tag, name: 'miss') - - results = described_class.search_for('match') - - expect(results).to eq [tag] - end - - it 'finds tag records in case insensitive' do - tag = Fabricate(:tag, name: 'MATCH') - _miss_tag = Fabricate(:tag, name: 'miss') - - results = described_class.search_for('match') - - expect(results).to eq [tag] - end - - it 'finds the exact matching tag as the first item' do - similar_tag = Fabricate(:tag, name: 'matchlater', reviewed_at: Time.now.utc) - tag = Fabricate(:tag, name: 'match', reviewed_at: Time.now.utc) - - results = described_class.search_for('match') - - expect(results).to eq [tag, similar_tag] - end - - it 'finds only listable tags' do - tag = Fabricate(:tag, name: 'match') - _miss_tag = Fabricate(:tag, name: 'matchunlisted', listable: false) - - results = described_class.search_for('match') - - expect(results).to eq [tag] - end - - it 'finds non-listable tags as well via option' do - tag = Fabricate(:tag, name: 'match') - unlisted_tag = Fabricate(:tag, name: 'matchunlisted', listable: false) - - results = described_class.search_for('match', 5, 0, exclude_unlistable: false) - - expect(results).to eq [tag, unlisted_tag] - end - end -end diff --git a/spec/models/trends/statuses_spec.rb b/spec/models/trends/statuses_spec.rb deleted file mode 100644 index 7c30b5b997..0000000000 --- a/spec/models/trends/statuses_spec.rb +++ /dev/null @@ -1,115 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Trends::Statuses do - subject! { described_class.new(threshold: 5, review_threshold: 10, score_halflife: 8.hours) } - - let!(:at_time) { DateTime.new(2021, 11, 14, 10, 15, 0) } - - describe 'Trends::Statuses::Query' do - let!(:query) { subject.query } - let!(:today) { at_time } - - let!(:status_foo) { Fabricate(:status, text: 'Foo', language: 'en', trendable: true, created_at: today) } - let!(:status_bar) { Fabricate(:status, text: 'Bar', language: 'en', trendable: true, created_at: today) } - - before do - default_threshold_value.times { reblog(status_foo, today) } - default_threshold_value.times { reblog(status_bar, today) } - - subject.refresh(today) - end - - describe '#filtered_for' do - let(:account) { Fabricate(:account) } - - it 'returns a composable query scope' do - expect(query.filtered_for(account)).to be_a Trends::Query - end - - it 'filters out blocked accounts' do - account.block!(status_foo.account) - expect(query.filtered_for(account).to_a).to eq [status_bar] - end - - it 'filters out muted accounts' do - account.mute!(status_bar.account) - expect(query.filtered_for(account).to_a).to eq [status_foo] - end - - it 'filters out blocked-by accounts' do - status_foo.account.block!(account) - expect(query.filtered_for(account).to_a).to eq [status_bar] - end - end - end - - describe '#add' do - let(:status) { Fabricate(:status) } - - before do - subject.add(status, 1, at_time) - end - - it 'records use' do - expect(subject.send(:recently_used_ids, at_time)).to eq [status.id] - end - end - - describe '#query' do - it 'returns a composable query scope' do - expect(subject.query).to be_a Trends::Query - end - - it 'responds to filtered_for' do - expect(subject.query).to respond_to(:filtered_for) - end - end - - describe '#refresh' do - let!(:today) { at_time } - let!(:yesterday) { today - 1.day } - - let!(:status_foo) { Fabricate(:status, text: 'Foo', language: 'en', trendable: true, created_at: yesterday) } - let!(:status_bar) { Fabricate(:status, text: 'Bar', language: 'en', trendable: true, created_at: today) } - let!(:status_baz) { Fabricate(:status, text: 'Baz', language: 'en', trendable: true, created_at: today) } - - before do - default_threshold_value.times { reblog(status_foo, today) } - default_threshold_value.times { reblog(status_bar, today) } - (default_threshold_value - 1).times { reblog(status_baz, today) } - end - - context 'when status trends are refreshed' do - before do - subject.refresh(today) - end - - it 'returns correct statuses from query' do - results = subject.query.limit(10).to_a - - expect(results).to eq [status_bar, status_foo] - expect(results).to_not include(status_baz) - end - end - - it 'decays scores' do - subject.refresh(today) - original_score = status_bar.trend.score - expect(original_score).to be_a Float - subject.refresh(today + subject.options[:score_halflife]) - decayed_score = status_bar.trend.reload.score - expect(decayed_score).to be <= original_score / 2 - end - end - - def reblog(status, at_time) - reblog = Fabricate(:status, reblog: status, created_at: at_time) - subject.add(status, reblog.account_id, at_time) - end - - def default_threshold_value - described_class.default_options[:threshold] - end -end diff --git a/spec/models/trends/tags_spec.rb b/spec/models/trends/tags_spec.rb deleted file mode 100644 index f2818fca87..0000000000 --- a/spec/models/trends/tags_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Trends::Tags do - subject { described_class.new(threshold: 5, review_threshold: 10) } - - let!(:at_time) { DateTime.new(2021, 11, 14, 10, 15, 0) } - - describe '#add' do - let(:tag) { Fabricate(:tag) } - - before do - subject.add(tag, 1, at_time) - end - - it 'records history' do - expect(tag.history.get(at_time).accounts).to eq 1 - end - - it 'records use' do - expect(subject.send(:recently_used_ids, at_time)).to eq [tag.id] - end - end - - describe '#query' do - it 'returns a composable query scope' do - expect(subject.query).to be_a Trends::Query - end - end - - describe '#refresh' do - let!(:today) { at_time } - let!(:yesterday) { today - 1.day } - - let!(:tag_cats) { Fabricate(:tag, name: 'Catstodon', trendable: true) } - let!(:tag_dogs) { Fabricate(:tag, name: 'DogsOfMastodon', trendable: true) } - let!(:tag_ocs) { Fabricate(:tag, name: 'OCs', trendable: true) } - - before do - 2.times { |i| subject.add(tag_cats, i, yesterday) } - 13.times { |i| subject.add(tag_ocs, i, yesterday) } - 16.times { |i| subject.add(tag_cats, i, today) } - 4.times { |i| subject.add(tag_dogs, i, today) } - end - - context 'when tag trends are refreshed' do - before do - subject.refresh(yesterday + 12.hours) - subject.refresh(at_time) - end - - it 'calculates and re-calculates scores' do - expect(subject.query.limit(10).to_a).to eq [tag_cats, tag_ocs] - end - - it 'omits hashtags below threshold' do - expect(subject.query.limit(10).to_a).to_not include(tag_dogs) - end - end - - it 'decays scores' do - subject.refresh(yesterday + 12.hours) - original_score = subject.score(tag_ocs.id) - expect(original_score).to eq 144.0 - subject.refresh(yesterday + 12.hours + subject.options[:max_score_halflife]) - decayed_score = subject.score(tag_ocs.id) - expect(decayed_score).to be <= original_score / 2 - end - end -end diff --git a/spec/models/user_role_spec.rb b/spec/models/user_role_spec.rb deleted file mode 100644 index 4ab66c3260..0000000000 --- a/spec/models/user_role_spec.rb +++ /dev/null @@ -1,189 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UserRole do - subject { described_class.create(name: 'Foo', position: 1) } - - describe '#can?' do - context 'with a single flag' do - it 'returns true if any of them are present' do - subject.permissions = described_class::FLAGS[:manage_reports] - expect(subject.can?(:manage_reports)).to be true - end - - it 'returns false if it is not set' do - expect(subject.can?(:manage_reports)).to be false - end - end - - context 'with multiple flags' do - it 'returns true if any of them are present' do - subject.permissions = described_class::FLAGS[:manage_users] - expect(subject.can?(:manage_reports, :manage_users)).to be true - end - - it 'returns false if none of them are present' do - expect(subject.can?(:manage_reports, :manage_users)).to be false - end - end - - context 'with an unknown flag' do - it 'raises an error' do - expect { subject.can?(:foo) }.to raise_error ArgumentError - end - end - end - - describe '#overrides?' do - it 'returns true if other role has lower position' do - expect(subject.overrides?(described_class.new(position: subject.position - 1))).to be true - end - - it 'returns true if other role is nil' do - expect(subject.overrides?(nil)).to be true - end - - it 'returns false if other role has higher position' do - expect(subject.overrides?(described_class.new(position: subject.position + 1))).to be false - end - end - - describe '#permissions_as_keys' do - before do - subject.permissions = described_class::FLAGS[:invite_users] | described_class::FLAGS[:view_dashboard] | described_class::FLAGS[:manage_reports] - end - - it 'returns an array' do - expect(subject.permissions_as_keys).to match_array %w(invite_users view_dashboard manage_reports) - end - end - - describe '#permissions_as_keys=' do - let(:input) { nil } - - before do - subject.permissions_as_keys = input - end - - context 'with a single value' do - let(:input) { %w(manage_users) } - - it 'sets permission flags' do - expect(subject.permissions).to eq described_class::FLAGS[:manage_users] - end - end - - context 'with multiple values' do - let(:input) { %w(manage_users manage_reports) } - - it 'sets permission flags' do - expect(subject.permissions).to eq described_class::FLAGS[:manage_users] | described_class::FLAGS[:manage_reports] - end - end - - context 'with an unknown value' do - let(:input) { %w(foo) } - - it 'does not set permission flags' do - expect(subject.permissions).to eq described_class::Flags::NONE - end - end - end - - describe '#computed_permissions' do - context 'when the role is nobody' do - subject { described_class.nobody } - - it 'returns none' do - expect(subject.computed_permissions).to eq described_class::Flags::NONE - end - end - - context 'when the role is everyone' do - subject { described_class.everyone } - - it 'returns permissions' do - expect(subject.computed_permissions).to eq subject.permissions - end - end - - context 'when role has the administrator flag' do - before do - subject.permissions = described_class::FLAGS[:administrator] - end - - it 'returns all permissions' do - expect(subject.computed_permissions).to eq described_class::Flags::ALL - end - end - - it 'returns permissions combined with the everyone role' do - expect(subject.computed_permissions).to eq described_class.everyone.permissions - end - end - - describe '.everyone' do - subject { described_class.everyone } - - it 'returns a role' do - expect(subject).to be_a(described_class) - end - - it 'is identified as the everyone role' do - expect(subject.everyone?).to be true - end - - it 'has default permissions' do - expect(subject.permissions).to eq described_class::FLAGS[:invite_users] - end - - it 'has negative position' do - expect(subject.position).to eq(described_class::NOBODY_POSITION) - end - end - - describe '.nobody' do - subject { described_class.nobody } - - it 'returns a role' do - expect(subject).to be_a(described_class) - end - - it 'is identified as the nobody role' do - expect(subject.nobody?).to be true - end - - it 'has no permissions' do - expect(subject.permissions).to eq described_class::Flags::NONE - end - - it 'has negative position' do - expect(subject.position).to eq(described_class::NOBODY_POSITION) - end - end - - describe '#everyone?' do - it 'returns true when id matches the everyone id' do - subject.id = described_class::EVERYONE_ROLE_ID - expect(subject.everyone?).to be true - end - - it 'returns false when id does not match the everyone id' do - subject.id = 123 - expect(subject.everyone?).to be false - end - end - - describe '#nobody?' do - it 'returns true when id is nil' do - subject.id = nil - expect(subject.nobody?).to be true - end - - it 'returns false when id is not nil' do - subject.id = 123 - expect(subject.nobody?).to be false - end - end -end diff --git a/spec/models/user_settings/namespace_spec.rb b/spec/models/user_settings/namespace_spec.rb deleted file mode 100644 index ae2fa7b482..0000000000 --- a/spec/models/user_settings/namespace_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UserSettings::Namespace do - subject { described_class.new(name) } - - let(:name) { :foo } - - describe '#setting' do - before do - subject.setting :bar, default: 'baz' - end - - it 'adds setting to definitions' do - expect(subject.definitions[:'foo.bar']).to have_attributes(name: :bar, namespace: :foo, default_value: 'baz') - end - end - - describe '#definitions' do - it 'returns a hash' do - expect(subject.definitions).to be_a Hash - end - end -end diff --git a/spec/models/user_settings/setting_spec.rb b/spec/models/user_settings/setting_spec.rb deleted file mode 100644 index 8c8d31ec54..0000000000 --- a/spec/models/user_settings/setting_spec.rb +++ /dev/null @@ -1,106 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UserSettings::Setting do - subject { described_class.new(name, options) } - - let(:name) { :foo } - let(:options) { { default: default, namespace: namespace } } - let(:default) { false } - let(:namespace) { nil } - - describe '#default_value' do - context 'when default value is a primitive value' do - it 'returns default value' do - expect(subject.default_value).to eq default - end - end - - context 'when default value is a proc' do - let(:default) { -> { 'bar' } } - - it 'returns value from proc' do - expect(subject.default_value).to eq 'bar' - end - end - end - - describe '#type' do - it 'returns a type' do - expect(subject.type).to be_a ActiveModel::Type::Value - end - - context 'when default value is a boolean' do - let(:default) { false } - - it 'returns boolean' do - expect(subject.type).to be_a ActiveModel::Type::Boolean - end - end - - context 'when default value is a string' do - let(:default) { '' } - - it 'returns string' do - expect(subject.type).to be_a ActiveModel::Type::String - end - end - - context 'when default value is a lambda returning a boolean' do - let(:default) { -> { false } } - - it 'returns boolean' do - expect(subject.type).to be_a ActiveModel::Type::Boolean - end - end - - context 'when default value is a lambda returning a string' do - let(:default) { -> { '' } } - - it 'returns boolean' do - expect(subject.type).to be_a ActiveModel::Type::String - end - end - end - - describe '#type_cast' do - context 'when default value is a boolean' do - let(:default) { false } - - it 'returns boolean' do - expect(subject.type_cast('1')).to be true - end - end - - context 'when default value is a string' do - let(:default) { '' } - - it 'returns string' do - expect(subject.type_cast(1)).to eq '1' - end - end - end - - describe '#to_a' do - it 'returns an array' do - expect(subject.to_a).to eq [name, default] - end - end - - describe '#key' do - context 'when there is no namespace' do - it 'returns a symbol' do - expect(subject.key).to eq :foo - end - end - - context 'when there is a namespace' do - let(:namespace) { :bar } - - it 'returns a symbol' do - expect(subject.key).to eq :'bar.foo' - end - end - end -end diff --git a/spec/models/user_settings_spec.rb b/spec/models/user_settings_spec.rb deleted file mode 100644 index dfc4910d6e..0000000000 --- a/spec/models/user_settings_spec.rb +++ /dev/null @@ -1,120 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UserSettings do - subject { described_class.new(json) } - - let(:json) { {} } - - describe '#[]' do - context 'when setting is not set' do - it 'returns default value' do - expect(subject[:always_send_emails]).to be false - end - end - - context 'when setting is set' do - let(:json) { { default_language: 'fr' } } - - it 'returns value' do - expect(subject[:default_language]).to eq 'fr' - end - end - - context 'when setting was not defined' do - it 'raises error' do - expect { subject[:foo] }.to raise_error described_class::KeyError - end - end - end - - describe '#[]=' do - context 'when value matches type' do - before do - subject[:always_send_emails] = true - end - - it 'updates value' do - expect(subject[:always_send_emails]).to be true - end - end - - context 'when value needs to be type-cast' do - before do - subject[:always_send_emails] = '1' - end - - it 'updates value with a type-cast' do - expect(subject[:always_send_emails]).to be true - end - end - - context 'when the setting has a closed set of values' do - it 'updates the attribute when given a valid value' do - expect { subject[:'web.display_media'] = :show_all }.to change { subject[:'web.display_media'] }.from('default').to('show_all') - end - - it 'raises an error when given an invalid value' do - expect { subject[:'web.display_media'] = 'invalid value' }.to raise_error ArgumentError - end - end - end - - describe '#update' do - before do - subject.update(always_send_emails: true, default_language: 'fr', default_privacy: nil) - end - - it 'updates values' do - expect(subject[:always_send_emails]).to be true - expect(subject[:default_language]).to eq 'fr' - end - - it 'does not set values that are nil' do - expect(subject.as_json).to_not include(default_privacy: nil) - end - end - - describe '#as_json' do - let(:json) { { default_language: 'fr' } } - - it 'returns hash' do - expect(subject.as_json).to eq json - end - end - - describe '.keys' do - it 'returns an array' do - expect(described_class.keys).to be_a Array - end - end - - describe '.definition_for' do - context 'when key is defined' do - it 'returns a setting' do - expect(described_class.definition_for(:always_send_emails)).to be_a described_class::Setting - end - end - - context 'when key is not defined' do - it 'returns nil' do - expect(described_class.definition_for(:foo)).to be_nil - end - end - end - - describe '.definition_for?' do - context 'when key is defined' do - it 'returns true' do - expect(described_class.definition_for?(:always_send_emails)).to be true - end - end - - context 'when key is not defined' do - it 'returns false' do - expect(described_class.definition_for?(:foo)).to be false - end - end - end -end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb deleted file mode 100644 index 4755500fc4..0000000000 --- a/spec/models/user_spec.rb +++ /dev/null @@ -1,599 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'devise_two_factor/spec_helpers' - -RSpec.describe User do - let(:password) { 'abcd1234' } - let(:account) { Fabricate(:account, username: 'alice') } - - it_behaves_like 'two_factor_backupable' - - describe 'legacy_otp_secret' do - it 'is encrypted with OTP_SECRET environment variable' do - user = Fabricate(:user, - encrypted_otp_secret: "Fttsy7QAa0edaDfdfSz094rRLAxc8cJweDQ4BsWH/zozcdVA8o9GLqcKhn2b\nGi/V\n", - encrypted_otp_secret_iv: 'rys3THICkr60BoWC', - encrypted_otp_secret_salt: '_LMkAGvdg7a+sDIKjI3mR2Q==') - - expect(user.send(:legacy_otp_secret)).to eq 'anotpsecretthatshouldbeencrypted' - end - end - - describe 'otp_secret' do - it 'encrypts the saved value' do - user = Fabricate(:user, otp_secret: '123123123') - - user.reload - - expect(user.otp_secret).to eq '123123123' - expect(user.attributes_before_type_cast[:otp_secret]).to_not eq '123123123' - end - end - - describe 'validations' do - it 'is invalid without an account' do - user = Fabricate.build(:user, account: nil) - user.valid? - expect(user).to model_have_error_on_field(:account) - end - - it 'is invalid without a valid email' do - user = Fabricate.build(:user, email: 'john@') - user.valid? - expect(user).to model_have_error_on_field(:email) - end - - it 'is valid with an invalid e-mail that has already been saved' do - user = Fabricate.build(:user, email: 'invalid-email') - user.save(validate: false) - expect(user.valid?).to be true - end - - it 'is valid with a localhost e-mail address' do - user = Fabricate.build(:user, email: 'admin@localhost') - user.valid? - expect(user.valid?).to be true - end - end - - describe 'Normalizations' do - describe 'locale' do - it 'preserves valid locale' do - user = Fabricate.build(:user, locale: 'en') - - expect(user.locale).to eq('en') - end - - it 'cleans out invalid locale' do - user = Fabricate.build(:user, locale: 'toto') - - expect(user.locale).to be_nil - end - end - - describe 'time_zone' do - it 'preserves valid timezone' do - user = Fabricate.build(:user, time_zone: 'UTC') - - expect(user.time_zone).to eq('UTC') - end - - it 'cleans out invalid timezone' do - user = Fabricate.build(:user, time_zone: 'toto') - - expect(user.time_zone).to be_nil - end - end - - describe 'languages' do - it 'preserves valid options for languages' do - user = Fabricate.build(:user, chosen_languages: ['en', 'fr', '']) - - expect(user.chosen_languages).to eq(['en', 'fr']) - end - - it 'cleans out empty string from languages' do - user = Fabricate.build(:user, chosen_languages: ['']) - - expect(user.chosen_languages).to be_nil - end - end - end - - describe 'scopes', :inline_jobs do - describe 'recent' do - it 'returns an array of recent users ordered by id' do - first_user = Fabricate(:user) - second_user = Fabricate(:user) - expect(described_class.recent).to eq [second_user, first_user] - end - end - - describe 'confirmed' do - it 'returns an array of users who are confirmed' do - Fabricate(:user, confirmed_at: nil) - confirmed_user = Fabricate(:user, confirmed_at: Time.zone.now) - expect(described_class.confirmed).to contain_exactly(confirmed_user) - end - end - - describe 'signed_in_recently' do - it 'returns a relation of users who have signed in during the recent period' do - recent_sign_in_user = Fabricate(:user, current_sign_in_at: within_duration_window_days.ago) - Fabricate(:user, current_sign_in_at: exceed_duration_window_days.ago) - - expect(described_class.signed_in_recently) - .to contain_exactly(recent_sign_in_user) - end - end - - describe 'not_signed_in_recently' do - it 'returns a relation of users who have not signed in during the recent period' do - no_recent_sign_in_user = Fabricate(:user, current_sign_in_at: exceed_duration_window_days.ago) - Fabricate(:user, current_sign_in_at: within_duration_window_days.ago) - - expect(described_class.not_signed_in_recently) - .to contain_exactly(no_recent_sign_in_user) - end - end - - describe 'account_not_suspended' do - it 'returns with linked accounts that are not suspended' do - suspended_account = Fabricate(:account, suspended_at: 10.days.ago) - non_suspended_account = Fabricate(:account, suspended_at: nil) - suspended_user = Fabricate(:user, account: suspended_account) - non_suspended_user = Fabricate(:user, account: non_suspended_account) - - expect(described_class.account_not_suspended) - .to include(non_suspended_user) - .and not_include(suspended_user) - end - end - - describe 'matches_email' do - it 'returns a relation of users whose email starts with the given string' do - specified = Fabricate(:user, email: 'specified@spec') - Fabricate(:user, email: 'unspecified@spec') - - expect(described_class.matches_email('specified')).to contain_exactly(specified) - end - end - - describe 'matches_ip' do - it 'returns a relation of users whose ip address is matching with the given CIDR' do - user1 = Fabricate(:user) - user2 = Fabricate(:user) - Fabricate(:session_activation, user: user1, ip: '2160:2160::22', session_id: '1') - Fabricate(:session_activation, user: user1, ip: '2160:2160::23', session_id: '2') - Fabricate(:session_activation, user: user2, ip: '2160:8888::24', session_id: '3') - Fabricate(:session_activation, user: user2, ip: '2160:8888::25', session_id: '4') - - expect(described_class.matches_ip('2160:2160::/32')).to contain_exactly(user1) - end - end - - def exceed_duration_window_days - described_class::ACTIVE_DURATION + 2.days - end - - def within_duration_window_days - described_class::ACTIVE_DURATION - 2.days - end - end - - describe 'blacklist' do - around do |example| - old_blacklist = Rails.configuration.x.email_blacklist - - Rails.configuration.x.email_domains_blacklist = 'mvrht.com' - - example.run - - Rails.configuration.x.email_domains_blacklist = old_blacklist - end - - it 'allows a non-blacklisted user to be created' do - user = described_class.new(email: 'foo@example.com', account: account, password: password, agreement: true) - - expect(user).to be_valid - end - - it 'does not allow a blacklisted user to be created' do - user = described_class.new(email: 'foo@mvrht.com', account: account, password: password, agreement: true) - - expect(user).to_not be_valid - end - - it 'does not allow a subdomain blacklisted user to be created' do - user = described_class.new(email: 'foo@mvrht.com.topdomain.tld', account: account, password: password, agreement: true) - - expect(user).to_not be_valid - end - end - - describe '#confirmed?' do - it 'returns true when a confirmed_at is set' do - user = Fabricate.build(:user, confirmed_at: Time.now.utc) - expect(user.confirmed?).to be true - end - - it 'returns false if a confirmed_at is nil' do - user = Fabricate.build(:user, confirmed_at: nil) - expect(user.confirmed?).to be false - end - end - - describe '#confirm' do - subject { user.confirm } - - let(:new_email) { 'new-email@example.com' } - - before do - allow(TriggerWebhookWorker).to receive(:perform_async) - end - - context 'when the user is already confirmed' do - let!(:user) { Fabricate(:user, confirmed_at: Time.now.utc, approved: true, unconfirmed_email: new_email) } - - it 'sets email to unconfirmed_email and does not trigger web hook' do - expect { subject }.to change { user.reload.email }.to(new_email) - - expect(TriggerWebhookWorker).to_not have_received(:perform_async).with('account.approved', 'Account', user.account_id) - end - end - - context 'when the user is a new user' do - let(:user) { Fabricate(:user, confirmed_at: nil, unconfirmed_email: new_email) } - - context 'when the user is already approved' do - before do - Setting.registrations_mode = 'approved' - user.approve! - end - - it 'sets email to unconfirmed_email and triggers `account.approved` web hook' do - expect { subject }.to change { user.reload.email }.to(new_email) - - expect(TriggerWebhookWorker).to have_received(:perform_async).with('account.approved', 'Account', user.account_id).once - end - end - - context 'when the user does not require explicit approval' do - before do - Setting.registrations_mode = 'open' - end - - it 'sets email to unconfirmed_email and triggers `account.approved` web hook' do - expect { subject }.to change { user.reload.email }.to(new_email) - - expect(TriggerWebhookWorker).to have_received(:perform_async).with('account.approved', 'Account', user.account_id).once - end - end - - context 'when the user requires explicit approval but is not approved' do - before do - Setting.registrations_mode = 'approved' - end - - it 'sets email to unconfirmed_email and does not trigger web hook' do - expect { subject }.to change { user.reload.email }.to(new_email) - - expect(TriggerWebhookWorker).to_not have_received(:perform_async).with('account.approved', 'Account', user.account_id) - end - end - end - end - - describe '#approve!' do - subject { user.approve! } - - before do - Setting.registrations_mode = 'approved' - allow(TriggerWebhookWorker).to receive(:perform_async) - end - - context 'when the user is already confirmed' do - let(:user) { Fabricate(:user, confirmed_at: Time.now.utc, approved: false) } - - it 'sets the approved flag and triggers `account.approved` web hook' do - expect { subject }.to change { user.reload.approved? }.to(true) - - expect(TriggerWebhookWorker).to have_received(:perform_async).with('account.approved', 'Account', user.account_id).once - end - end - - context 'when the user is not confirmed' do - let(:user) { Fabricate(:user, confirmed_at: nil, approved: false) } - - it 'sets the approved flag and does not trigger web hook' do - expect { subject }.to change { user.reload.approved? }.to(true) - - expect(TriggerWebhookWorker).to_not have_received(:perform_async).with('account.approved', 'Account', user.account_id) - end - end - end - - describe '#disable_two_factor!' do - it 'saves false for otp_required_for_login' do - user = Fabricate.build(:user, otp_required_for_login: true) - user.disable_two_factor! - expect(user.reload.otp_required_for_login).to be false - end - - it 'saves nil for otp_secret' do - user = Fabricate.build(:user, otp_secret: 'oldotpcode') - user.disable_two_factor! - expect(user.reload.otp_secret).to be_nil - end - - it 'saves cleared otp_backup_codes' do - user = Fabricate.build(:user, otp_backup_codes: %w(dummy dummy)) - user.disable_two_factor! - expect(user.reload.otp_backup_codes.empty?).to be true - end - end - - describe '#send_confirmation_instructions' do - around do |example| - queue_adapter = ActiveJob::Base.queue_adapter - example.run - ActiveJob::Base.queue_adapter = queue_adapter - end - - it 'delivers confirmation instructions later' do - user = Fabricate(:user) - ActiveJob::Base.queue_adapter = :test - - expect { user.send_confirmation_instructions }.to have_enqueued_job(ActionMailer::MailDeliveryJob) - end - end - - describe 'settings' do - it 'is instance of UserSettings' do - user = Fabricate(:user) - expect(user.settings).to be_a UserSettings - end - end - - describe '#setting_default_privacy' do - it 'returns default privacy setting if user has configured' do - user = Fabricate(:user) - user.settings[:default_privacy] = 'unlisted' - expect(user.setting_default_privacy).to eq 'unlisted' - end - - it "returns 'private' if user has not configured default privacy setting and account is locked" do - user = Fabricate(:account, locked: true).user - expect(user.setting_default_privacy).to eq 'private' - end - - it "returns 'public' if user has not configured default privacy setting and account is not locked" do - user = Fabricate(:account, locked: false).user - expect(user.setting_default_privacy).to eq 'public' - end - end - - describe 'whitelist' do - around do |example| - old_whitelist = Rails.configuration.x.email_domains_whitelist - - Rails.configuration.x.email_domains_whitelist = 'mastodon.space' - - example.run - - Rails.configuration.x.email_domains_whitelist = old_whitelist - end - - it 'does not allow a user to be created unless they are whitelisted' do - user = described_class.new(email: 'foo@example.com', account: account, password: password, agreement: true) - expect(user).to_not be_valid - end - - it 'allows a user to be created if they are whitelisted' do - user = described_class.new(email: 'foo@mastodon.space', account: account, password: password, agreement: true) - expect(user).to be_valid - end - - it 'does not allow a user with a whitelisted top domain as subdomain in their email address to be created' do - user = described_class.new(email: 'foo@mastodon.space.userdomain.com', account: account, password: password, agreement: true) - expect(user).to_not be_valid - end - - context 'with a blacklisted subdomain' do - around do |example| - old_blacklist = Rails.configuration.x.email_blacklist - example.run - Rails.configuration.x.email_domains_blacklist = old_blacklist - end - - it 'does not allow a user to be created with a specific blacklisted subdomain even if the top domain is whitelisted' do - Rails.configuration.x.email_domains_blacklist = 'blacklisted.mastodon.space' - - user = described_class.new(email: 'foo@blacklisted.mastodon.space', account: account, password: password) - expect(user).to_not be_valid - end - end - end - - describe 'token_for_app' do - let(:user) { Fabricate(:user) } - let(:app) { Fabricate(:application, owner: user) } - - it 'returns a token' do - expect(user.token_for_app(app)).to be_a(Doorkeeper::AccessToken) - end - - it 'persists a token' do - t = user.token_for_app(app) - expect(user.token_for_app(app)).to eql(t) - end - - it 'is nil if user does not own app' do - app.update!(owner: nil) - - expect(user.token_for_app(app)).to be_nil - end - end - - describe '#disable!' do - subject(:user) { Fabricate(:user, disabled: false, current_sign_in_at: current_sign_in_at, last_sign_in_at: nil) } - - let(:current_sign_in_at) { Time.zone.now } - - before do - user.disable! - end - - it 'disables user' do - expect(user).to have_attributes(disabled: true) - end - end - - describe '#enable!' do - subject(:user) { Fabricate(:user, disabled: true) } - - before do - user.enable! - end - - it 'enables user' do - expect(user).to have_attributes(disabled: false) - end - end - - describe '#reset_password!' do - subject(:user) { Fabricate(:user, password: 'foobar12345') } - - let!(:session_activation) { Fabricate(:session_activation, user: user) } - let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) } - let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) } - - let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) } - - before do - allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub) - user.reset_password! - end - - it 'changes the password immediately' do - expect(user.external_or_valid_password?('foobar12345')).to be false - end - - it 'deactivates all sessions' do - expect(user.session_activations.count).to eq 0 - expect { session_activation.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'revokes all access tokens' do - expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0 - end - - it 'revokes streaming access for all access tokens' do - expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", Oj.dump(event: :kill)).once - end - - it 'removes push subscriptions' do - expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0 - expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - describe '#mark_email_as_confirmed!' do - subject { user.mark_email_as_confirmed! } - - let!(:user) { Fabricate(:user, confirmed_at: confirmed_at) } - - context 'when user is new' do - let(:confirmed_at) { nil } - - it 'confirms user and delivers welcome email', :inline_jobs do - emails = capture_emails { subject } - - expect(user.confirmed_at).to be_present - expect(emails.size) - .to eq(1) - expect(emails.first) - .to have_attributes( - to: contain_exactly(user.email), - subject: eq(I18n.t('user_mailer.welcome.subject')) - ) - end - end - - context 'when user is not new' do - let(:confirmed_at) { Time.zone.now } - - it 'confirms user but does not deliver welcome email' do - emails = capture_emails { subject } - - expect(user.confirmed_at).to be_present - expect(emails).to be_empty - end - end - end - - describe '#active_for_authentication?' do - subject { user.active_for_authentication? } - - let(:user) { Fabricate(:user, disabled: disabled, confirmed_at: confirmed_at) } - - context 'when user is disabled' do - let(:disabled) { true } - - context 'when user is confirmed' do - let(:confirmed_at) { Time.zone.now } - - it { is_expected.to be true } - end - - context 'when user is not confirmed' do - let(:confirmed_at) { nil } - - it { is_expected.to be true } - end - end - - context 'when user is not disabled' do - let(:disabled) { false } - - context 'when user is confirmed' do - let(:confirmed_at) { Time.zone.now } - - it { is_expected.to be true } - end - - context 'when user is not confirmed' do - let(:confirmed_at) { nil } - - it { is_expected.to be true } - end - end - end - - describe '.those_who_can' do - before { Fabricate(:user, role: UserRole.find_by(name: 'Moderator')) } - - context 'when there are not any user roles' do - before { UserRole.destroy_all } - - it 'returns an empty list' do - expect(described_class.those_who_can(:manage_blocks)).to eq([]) - end - end - - context 'when there are not users with the needed role' do - it 'returns an empty list' do - expect(described_class.those_who_can(:manage_blocks)).to eq([]) - end - end - - context 'when there are users with roles' do - let!(:admin_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - - it 'returns the users with the role' do - expect(described_class.those_who_can(:manage_blocks)).to eq([admin_user]) - end - end - end -end diff --git a/spec/models/web/push_subscription_spec.rb b/spec/models/web/push_subscription_spec.rb deleted file mode 100644 index 3c2cd3bac1..0000000000 --- a/spec/models/web/push_subscription_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Web::PushSubscription do - subject { described_class.new(data: data) } - - let(:account) { Fabricate(:account) } - - let(:policy) { 'all' } - - let(:data) do - { - policy: policy, - - alerts: { - mention: true, - reblog: false, - follow: true, - follow_request: false, - favourite: true, - }, - } - end - - describe '#pushable?' do - let(:notification_type) { :mention } - let(:notification) { Fabricate(:notification, account: account, type: notification_type) } - - %i(mention reblog follow follow_request favourite).each do |type| - context "when notification is a #{type}" do - let(:notification_type) { type } - - it 'returns boolean corresponding to alert setting' do - expect(subject.pushable?(notification)).to eq data[:alerts][type] - end - end - end - - context 'when policy is all' do - let(:policy) { 'all' } - - it 'returns true' do - expect(subject.pushable?(notification)).to be true - end - end - - context 'when policy is none' do - let(:policy) { 'none' } - - it 'returns false' do - expect(subject.pushable?(notification)).to be false - end - end - - context 'when policy is followed' do - let(:policy) { 'followed' } - - context 'when notification is from someone you follow' do - before do - account.follow!(notification.from_account) - end - - it 'returns true' do - expect(subject.pushable?(notification)).to be true - end - end - - context 'when notification is not from someone you follow' do - it 'returns false' do - expect(subject.pushable?(notification)).to be false - end - end - end - - context 'when policy is follower' do - let(:policy) { 'follower' } - - context 'when notification is from someone who follows you' do - before do - notification.from_account.follow!(account) - end - - it 'returns true' do - expect(subject.pushable?(notification)).to be true - end - end - - context 'when notification is not from someone who follows you' do - it 'returns false' do - expect(subject.pushable?(notification)).to be false - end - end - end - end -end diff --git a/spec/models/webauthn_credential_spec.rb b/spec/models/webauthn_credential_spec.rb deleted file mode 100644 index 23f0229a67..0000000000 --- a/spec/models/webauthn_credential_spec.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe WebauthnCredential do - describe 'validations' do - it 'is invalid without an external id' do - webauthn_credential = Fabricate.build(:webauthn_credential, external_id: nil) - - webauthn_credential.valid? - - expect(webauthn_credential).to model_have_error_on_field(:external_id) - end - - it 'is invalid without a public key' do - webauthn_credential = Fabricate.build(:webauthn_credential, public_key: nil) - - webauthn_credential.valid? - - expect(webauthn_credential).to model_have_error_on_field(:public_key) - end - - it 'is invalid without a nickname' do - webauthn_credential = Fabricate.build(:webauthn_credential, nickname: nil) - - webauthn_credential.valid? - - expect(webauthn_credential).to model_have_error_on_field(:nickname) - end - - it 'is invalid without a sign_count' do - webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: nil) - - webauthn_credential.valid? - - expect(webauthn_credential).to model_have_error_on_field(:sign_count) - end - - it 'is invalid if already exist a webauthn credential with the same external id' do - Fabricate(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw') - new_webauthn_credential = Fabricate.build(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw') - - new_webauthn_credential.valid? - - expect(new_webauthn_credential).to model_have_error_on_field(:external_id) - end - - it 'is invalid if user already registered a webauthn credential with the same nickname' do - user = Fabricate(:user) - Fabricate(:webauthn_credential, user_id: user.id, nickname: 'USB Key') - new_webauthn_credential = Fabricate.build(:webauthn_credential, user_id: user.id, nickname: 'USB Key') - - new_webauthn_credential.valid? - - expect(new_webauthn_credential).to model_have_error_on_field(:nickname) - end - - it 'is invalid if sign_count is not a number' do - webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 'invalid sign_count') - - webauthn_credential.valid? - - expect(webauthn_credential).to model_have_error_on_field(:sign_count) - end - - it 'is invalid if sign_count is negative number' do - webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: -1) - - webauthn_credential.valid? - - expect(webauthn_credential).to model_have_error_on_field(:sign_count) - end - - it 'is invalid if sign_count is greater than the limit' do - webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: (described_class::SIGN_COUNT_LIMIT * 2)) - - webauthn_credential.valid? - - expect(webauthn_credential).to model_have_error_on_field(:sign_count) - end - end -end diff --git a/spec/models/webhook_spec.rb b/spec/models/webhook_spec.rb deleted file mode 100644 index effaf92e9c..0000000000 --- a/spec/models/webhook_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Webhook do - let(:webhook) { Fabricate(:webhook) } - - describe 'Validations' do - it 'requires presence of events' do - record = described_class.new(events: nil) - record.valid? - - expect(record).to model_have_error_on_field(:events) - end - - it 'requires non-empty events value' do - record = described_class.new(events: []) - record.valid? - - expect(record).to model_have_error_on_field(:events) - end - - it 'requires valid events value from EVENTS' do - record = described_class.new(events: ['account.invalid']) - record.valid? - - expect(record).to model_have_error_on_field(:events) - end - end - - describe 'Normalizations' do - it 'cleans up events values' do - record = described_class.new(events: ['account.approved', 'account.created ', '']) - - expect(record.events).to eq(%w(account.approved account.created)) - end - end - - describe '#rotate_secret!' do - it 'changes the secret' do - previous_value = webhook.secret - webhook.rotate_secret! - expect(webhook.secret).to_not be_blank - expect(webhook.secret).to_not eq previous_value - end - end - - describe '#enable!' do - before do - webhook.disable! - end - - it 'enables the webhook' do - webhook.enable! - expect(webhook.enabled?).to be true - end - end - - describe '#disable!' do - it 'disables the webhook' do - webhook.disable! - expect(webhook.enabled?).to be false - end - end -end diff --git a/spec/policies/account_moderation_note_policy_spec.rb b/spec/policies/account_moderation_note_policy_spec.rb deleted file mode 100644 index 8c37acc39f..0000000000 --- a/spec/policies/account_moderation_note_policy_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe AccountModerationNotePolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:account) } - - permissions :create? do - context 'when staff' do - it 'grants to create' do - expect(subject).to permit(admin, described_class) - end - end - - context 'when not staff' do - it 'denies to create' do - expect(subject).to_not permit(john, described_class) - end - end - end - - permissions :destroy? do - let(:account_moderation_note) do - Fabricate(:account_moderation_note, - account: john, - target_account: Fabricate(:account)) - end - - context 'when admin' do - it 'grants to destroy' do - expect(subject).to permit(admin, account_moderation_note) - end - end - - context 'when owner' do - it 'grants to destroy' do - expect(subject).to permit(john, account_moderation_note) - end - end - - context 'when neither admin nor owner' do - let(:kevin) { Fabricate(:account) } - - it 'denies to destroy' do - expect(subject).to_not permit(kevin, account_moderation_note) - end - end - end -end diff --git a/spec/policies/account_policy_spec.rb b/spec/policies/account_policy_spec.rb deleted file mode 100644 index d7a21d8e39..0000000000 --- a/spec/policies/account_policy_spec.rb +++ /dev/null @@ -1,160 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe AccountPolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:account) } - let(:alice) { Fabricate(:account) } - - permissions :index? do - context 'when staff' do - it 'permits' do - expect(subject).to permit(admin) - end - end - - context 'when not staff' do - it 'denies' do - expect(subject).to_not permit(john) - end - end - end - - permissions :show?, :unsilence?, :unsensitive?, :remove_avatar?, :remove_header? do - context 'when staff' do - it 'permits' do - expect(subject).to permit(admin, alice) - end - end - - context 'when not staff' do - it 'denies' do - expect(subject).to_not permit(john, alice) - end - end - end - - permissions :unsuspend?, :unblock_email? do - before do - alice.suspend! - end - - context 'when staff' do - it 'permits' do - expect(subject).to permit(admin, alice) - end - end - - context 'when not staff' do - it 'denies' do - expect(subject).to_not permit(john, alice) - end - end - end - - permissions :redownload? do - context 'when admin' do - it 'permits' do - expect(subject).to permit(admin) - end - end - - context 'when not admin' do - it 'denies' do - expect(subject).to_not permit(john) - end - end - end - - permissions :suspend?, :silence? do - let(:staff) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - - context 'when staff' do - context 'when record is staff' do - it 'denies' do - expect(subject).to_not permit(admin, staff) - end - end - - context 'when record is not staff' do - it 'permits' do - expect(subject).to permit(admin, john) - end - end - end - - context 'when not staff' do - it 'denies' do - expect(subject).to_not permit(john, Account) - end - end - end - - permissions :memorialize? do - let(:other_admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - - context 'when admin' do - context 'when record is admin' do - it 'denies' do - expect(subject).to_not permit(admin, other_admin) - end - end - - context 'when record is not admin' do - it 'permits' do - expect(subject).to permit(admin, john) - end - end - end - - context 'when not admin' do - it 'denies' do - expect(subject).to_not permit(john, Account) - end - end - end - - permissions :review? do - context 'when admin' do - it 'permits' do - expect(subject).to permit(admin) - end - end - - context 'when not admin' do - it 'denies' do - expect(subject).to_not permit(john) - end - end - end - - permissions :destroy? do - context 'when 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 'when 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 deleted file mode 100644 index 63bf33de24..0000000000 --- a/spec/policies/account_warning_preset_policy_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# 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 deleted file mode 100644 index af9f7716be..0000000000 --- a/spec/policies/admin/status_policy_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -# 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, visibility: status_visibility) } - let(:status_visibility) { :public } - - 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 - let(:status_visibility) { :public } - - it 'permits' do - expect(policy).to permit(admin, status) - end - end - - context 'with a not public visible status' do - let(:status_visibility) { :direct } - - it 'denies' do - expect(policy).to_not permit(admin, status) - end - - context 'when the status mentions the admin' do - before do - status.mentions.create!(account: admin) - end - - it 'permits' do - expect(policy).to permit(admin, status) - end - 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 deleted file mode 100644 index 3d230b3cb4..0000000000 --- a/spec/policies/announcement_policy_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# 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 deleted file mode 100644 index d7498eb9f0..0000000000 --- a/spec/policies/appeal_policy_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -# 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/backup_policy_spec.rb b/spec/policies/backup_policy_spec.rb deleted file mode 100644 index 28cb65d789..0000000000 --- a/spec/policies/backup_policy_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe BackupPolicy do - subject { described_class } - - let(:john) { Fabricate(:account) } - - permissions :create? do - context 'when not user_signed_in?' do - it 'denies' do - expect(subject).to_not permit(nil, Backup) - end - end - - context 'when user_signed_in?' do - context 'with no backups' do - it 'permits' do - expect(subject).to permit(john, Backup) - end - end - - context 'when backups are too old' do - it 'permits' do - travel(-8.days) do - Fabricate(:backup, user: john.user) - end - - expect(subject).to permit(john, Backup) - end - end - - context 'when backups are newer' do - it 'denies' do - travel(-3.days) do - Fabricate(:backup, user: john.user) - end - - expect(subject).to_not permit(john, Backup) - end - end - end - end -end diff --git a/spec/policies/canonical_email_block_policy_spec.rb b/spec/policies/canonical_email_block_policy_spec.rb deleted file mode 100644 index 0e55febfa9..0000000000 --- a/spec/policies/canonical_email_block_policy_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# 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/custom_emoji_policy_spec.rb b/spec/policies/custom_emoji_policy_spec.rb deleted file mode 100644 index cb869c7d9a..0000000000 --- a/spec/policies/custom_emoji_policy_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe CustomEmojiPolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:account) } - - permissions :index?, :enable?, :disable? do - context 'when staff' do - it 'permits' do - expect(subject).to permit(admin, CustomEmoji) - end - end - - context 'when not staff' do - it 'denies' do - expect(subject).to_not permit(john, CustomEmoji) - end - end - end - - permissions :create?, :update?, :copy?, :destroy? do - context 'when admin' do - it 'permits' do - expect(subject).to permit(admin, CustomEmoji) - end - end - - context 'when not admin' do - it 'denies' do - expect(subject).to_not permit(john, CustomEmoji) - end - end - end -end diff --git a/spec/policies/delivery_policy_spec.rb b/spec/policies/delivery_policy_spec.rb deleted file mode 100644 index fbcbf390d7..0000000000 --- a/spec/policies/delivery_policy_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# 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/domain_block_policy_spec.rb b/spec/policies/domain_block_policy_spec.rb deleted file mode 100644 index 4c89f3f374..0000000000 --- a/spec/policies/domain_block_policy_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe DomainBlockPolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:account) } - - permissions :index?, :show?, :create?, :destroy? do - context 'when admin' do - it 'permits' do - expect(subject).to permit(admin, DomainBlock) - end - end - - context 'when not admin' do - it 'denies' do - expect(subject).to_not permit(john, DomainBlock) - end - end - end -end diff --git a/spec/policies/email_domain_block_policy_spec.rb b/spec/policies/email_domain_block_policy_spec.rb deleted file mode 100644 index 7ecff4be49..0000000000 --- a/spec/policies/email_domain_block_policy_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe EmailDomainBlockPolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:account) } - - permissions :index?, :show?, :create?, :destroy? do - context 'when admin' do - it 'permits' do - expect(subject).to permit(admin, EmailDomainBlock) - end - end - - context 'when not admin' do - it 'denies' do - expect(subject).to_not permit(john, EmailDomainBlock) - end - end - end -end diff --git a/spec/policies/follow_recommendation_policy_spec.rb b/spec/policies/follow_recommendation_policy_spec.rb deleted file mode 100644 index 01f4da0be2..0000000000 --- a/spec/policies/follow_recommendation_policy_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# 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/instance_policy_spec.rb b/spec/policies/instance_policy_spec.rb deleted file mode 100644 index a0d9a008b7..0000000000 --- a/spec/policies/instance_policy_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe InstancePolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:account) } - - permissions :index?, :show?, :destroy? do - context 'when admin' do - it 'permits' do - expect(subject).to permit(admin, Instance) - end - end - - context 'when not admin' do - it 'denies' do - expect(subject).to_not permit(john, Instance) - end - end - end -end diff --git a/spec/policies/invite_policy_spec.rb b/spec/policies/invite_policy_spec.rb deleted file mode 100644 index cbe3735d80..0000000000 --- a/spec/policies/invite_policy_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe InvitePolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:user).account } - - permissions :index? do - context 'when staff?' do - it 'permits' do - expect(subject).to permit(admin, Invite) - end - end - end - - permissions :create? do - context 'with privilege' do - before do - UserRole.everyone.update(permissions: UserRole::FLAGS[:invite_users]) - end - - it 'permits' do - expect(subject).to permit(john, Invite) - end - end - - context 'when does not have privilege' do - before do - UserRole.everyone.update(permissions: UserRole::Flags::NONE) - end - - it 'denies' do - expect(subject).to_not permit(john, Invite) - end - end - end - - permissions :deactivate_all? do - context 'when admin?' do - it 'permits' do - expect(subject).to permit(admin, Invite) - end - end - - context 'when not admin?' do - it 'denies' do - expect(subject).to_not permit(john, Invite) - end - end - end - - permissions :destroy? do - context 'when owner?' do - it 'permits' do - expect(subject).to permit(john, Fabricate(:invite, user: john.user)) - end - end - - context 'when not owner?' do - context 'when admin?' do - it 'permits' do - expect(subject).to permit(admin, Fabricate(:invite)) - end - end - - context 'when not admin?' do - it 'denies' do - expect(subject).to_not permit(john, Fabricate(:invite)) - end - end - end - end -end diff --git a/spec/policies/ip_block_policy_spec.rb b/spec/policies/ip_block_policy_spec.rb deleted file mode 100644 index 3cfa85863c..0000000000 --- a/spec/policies/ip_block_policy_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# 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 deleted file mode 100644 index d6675c5b34..0000000000 --- a/spec/policies/preview_card_policy_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# 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 deleted file mode 100644 index 8d3715de95..0000000000 --- a/spec/policies/preview_card_provider_policy_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# 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/relay_policy_spec.rb b/spec/policies/relay_policy_spec.rb deleted file mode 100644 index 29ba02c26a..0000000000 --- a/spec/policies/relay_policy_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe RelayPolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:account) } - - permissions :update? do - context 'when admin?' do - it 'permits' do - expect(subject).to permit(admin, Relay) - end - end - - context 'with !admin?' do - it 'denies' do - expect(subject).to_not permit(john, Relay) - end - end - end -end diff --git a/spec/policies/report_note_policy_spec.rb b/spec/policies/report_note_policy_spec.rb deleted file mode 100644 index b40a878887..0000000000 --- a/spec/policies/report_note_policy_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe ReportNotePolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:account) } - - permissions :create? do - context 'when staff?' do - it 'permits' do - expect(subject).to permit(admin, ReportNote) - end - end - - context 'with !staff?' do - it 'denies' do - expect(subject).to_not permit(john, ReportNote) - end - end - end - - permissions :destroy? do - context 'when admin?' do - it 'permit' do - report_note = Fabricate(:report_note, account: john) - expect(subject).to permit(admin, report_note) - end - end - - context 'when owner?' do - it 'permit' do - report_note = Fabricate(:report_note, account: john) - expect(subject).to permit(john, report_note) - end - end - - context 'with !owner?' do - it 'denies' do - report_note = Fabricate(:report_note) - expect(subject).to_not permit(john, report_note) - end - end - end -end diff --git a/spec/policies/report_policy_spec.rb b/spec/policies/report_policy_spec.rb deleted file mode 100644 index 4fc4178075..0000000000 --- a/spec/policies/report_policy_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe ReportPolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:account) } - - permissions :update?, :index?, :show? do - context 'when staff?' do - it 'permits' do - expect(subject).to permit(admin, Report) - end - end - - context 'with !staff?' do - it 'denies' do - expect(subject).to_not permit(john, Report) - end - end - end -end diff --git a/spec/policies/rule_policy_spec.rb b/spec/policies/rule_policy_spec.rb deleted file mode 100644 index 0e45f6df02..0000000000 --- a/spec/policies/rule_policy_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# 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 deleted file mode 100644 index 4a99314905..0000000000 --- a/spec/policies/settings_policy_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe SettingsPolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:account) } - - permissions :update?, :show?, :destroy? do - context 'when admin?' do - it 'permits' do - expect(subject).to permit(admin, Settings) - end - end - - context 'with !admin?' do - it 'denies' do - expect(subject).to_not permit(john, Settings) - end - end - end -end diff --git a/spec/policies/software_update_policy_spec.rb b/spec/policies/software_update_policy_spec.rb deleted file mode 100644 index e19ba61612..0000000000 --- a/spec/policies/software_update_policy_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe SoftwareUpdatePolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')).account } - let(:john) { Fabricate(:account) } - - permissions :index? do - context 'when owner' do - it 'permits' do - expect(subject).to permit(admin, SoftwareUpdate) - end - end - - context 'when not owner' do - it 'denies' do - expect(subject).to_not permit(john, SoftwareUpdate) - end - end - end -end diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb deleted file mode 100644 index 725bd0bbb3..0000000000 --- a/spec/policies/status_policy_spec.rb +++ /dev/null @@ -1,161 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe StatusPolicy, type: :model do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - let(:alice) { Fabricate(:account, username: 'alice') } - let(:bob) { Fabricate(:account, username: 'bob') } - let(:status) { Fabricate(:status, account: alice) } - - context 'with the permissions of show? and reblog?' do - permissions :show?, :reblog? do - it 'grants access when no viewer' do - expect(subject).to permit(nil, status) - end - - it 'denies access when viewer is blocked' do - block = Fabricate(:block) - status.visibility = :private - status.account = block.target_account - - expect(subject).to_not permit(block.account, status) - end - end - end - - context 'with the permission of show?' do - permissions :show? do - it 'grants access when direct and account is viewer' do - status.visibility = :direct - - expect(subject).to permit(status.account, status) - end - - it 'grants access when direct and viewer is mentioned' do - status.visibility = :direct - status.mentions = [Fabricate(:mention, account: alice)] - - expect(subject).to permit(alice, status) - end - - 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 - - expect(subject).to_not permit(viewer, status) - end - - it 'grants access when private and account is viewer' do - status.visibility = :private - - expect(subject).to permit(status.account, status) - end - - it 'grants access when private and account is following viewer' do - follow = Fabricate(:follow) - status.visibility = :private - status.account = follow.target_account - - expect(subject).to permit(follow.account, status) - end - - it 'grants access when private and viewer is mentioned' do - status.visibility = :private - status.mentions = [Fabricate(:mention, account: alice)] - - expect(subject).to permit(alice, status) - end - - it 'denies access when private and viewer is not mentioned or followed' do - viewer = Fabricate(:account) - status.visibility = :private - - expect(subject).to_not permit(viewer, status) - end - - it 'denies access when local-only and the viewer is not logged in' do - allow(status).to receive(:local_only?).and_return(true) - - expect(subject).to_not permit(nil, status) - end - - it 'denies access when local-only and the viewer is from another domain' do - viewer = Fabricate(:account, domain: 'remote-domain') - allow(status).to receive(:local_only?).and_return(true) - expect(subject).to_not permit(viewer, status) - end - end - end - - context 'with the permission of reblog?' do - permissions :reblog? do - it 'denies access when private' do - viewer = Fabricate(:account) - status.visibility = :private - - expect(subject).to_not permit(viewer, status) - end - - it 'denies access when direct' do - viewer = Fabricate(:account) - status.visibility = :direct - - expect(subject).to_not permit(viewer, status) - end - end - end - - context 'with the permissions of destroy? and unreblog?' do - permissions :destroy?, :unreblog? do - it 'grants access when account is deleter' do - expect(subject).to permit(status.account, status) - end - - it 'denies access when account is not deleter' do - expect(subject).to_not permit(bob, status) - end - - it 'denies access when no deleter' do - expect(subject).to_not permit(nil, status) - end - end - end - - context 'with the permission of favourite?' do - permissions :favourite? do - it 'grants access when viewer is not blocked' do - follow = Fabricate(:follow) - status.account = follow.target_account - - expect(subject).to permit(follow.account, status) - end - - it 'denies when viewer is blocked' do - block = Fabricate(:block) - status.account = block.target_account - - expect(subject).to_not permit(block.account, status) - end - end - end - - context 'with the permission of update?' do - permissions :update? do - it 'grants access if owner' do - expect(subject).to permit(status.account, status) - end - end - end -end diff --git a/spec/policies/tag_policy_spec.rb b/spec/policies/tag_policy_spec.rb deleted file mode 100644 index 35da3cc62a..0000000000 --- a/spec/policies/tag_policy_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe TagPolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:account) } - - permissions :index?, :show?, :update?, :review? do - context 'when staff?' do - it 'permits' do - expect(subject).to permit(admin, Tag) - end - end - - context 'with !staff?' do - it 'denies' do - expect(subject).to_not permit(john, Tag) - end - end - end -end diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb deleted file mode 100644 index 7854547d26..0000000000 --- a/spec/policies/user_policy_spec.rb +++ /dev/null @@ -1,115 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe UserPolicy do - subject { described_class } - - let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account } - let(:john) { Fabricate(:account) } - - permissions :reset_password?, :change_email? do - context 'when staff?' do - context 'with !record.staff?' do - it 'permits' do - expect(subject).to permit(admin, john.user) - end - end - - context 'when record.staff?' do - it 'denies' do - expect(subject).to_not permit(admin, admin.user) - end - end - end - - context 'with !staff?' do - it 'denies' do - expect(subject).to_not permit(john, User) - end - end - end - - permissions :disable_2fa? do - context 'when admin?' do - context 'with !record.staff?' do - it 'permits' do - expect(subject).to permit(admin, john.user) - end - end - - context 'when record.staff?' do - it 'denies' do - expect(subject).to_not permit(admin, admin.user) - end - end - end - - context 'with !admin?' do - it 'denies' do - expect(subject).to_not permit(john, User) - end - end - end - - permissions :confirm? do - context 'when staff?' do - context 'with !record.confirmed?' do - it 'permits' do - john.user.update(confirmed_at: nil) - expect(subject).to permit(admin, john.user) - end - end - - context 'when record.confirmed?' do - it 'denies' do - john.user.mark_email_as_confirmed! - expect(subject).to_not permit(admin, john.user) - end - end - end - - context 'with !staff?' do - it 'denies' do - expect(subject).to_not permit(john, User) - end - end - end - - permissions :enable? do - context 'when staff?' do - it 'permits' do - expect(subject).to permit(admin, User) - end - end - - context 'with !staff?' do - it 'denies' do - expect(subject).to_not permit(john, User) - end - end - end - - permissions :disable? do - context 'when staff?' do - context 'with !record.admin?' do - it 'permits' do - expect(subject).to permit(admin, john.user) - end - end - - context 'when record.admin?' do - it 'denies' do - expect(subject).to_not permit(admin, admin.user) - end - end - end - - context 'with !staff?' do - it 'denies' do - expect(subject).to_not permit(john, User) - end - end - end -end diff --git a/spec/policies/webhook_policy_spec.rb b/spec/policies/webhook_policy_spec.rb deleted file mode 100644 index 909311461a..0000000000 --- a/spec/policies/webhook_policy_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# 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? do - context 'with an admin' do - it 'permits' do - expect(policy).to permit(admin, Webhook) - end - end - - context 'with a non-admin' do - it 'denies' do - expect(policy).to_not permit(john, Webhook) - end - end - end - - permissions :show?, :update?, :enable?, :disable?, :rotate_secret?, :destroy? do - let(:webhook) { Fabricate(:webhook, events: ['account.created', 'report.created']) } - - context 'with an admin' do - it 'permits' do - expect(policy).to permit(admin, webhook) - end - end - - context 'with a non-admin' do - it 'denies' do - expect(policy).to_not permit(john, webhook) - end - end - end -end diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb deleted file mode 100644 index 282cae4f06..0000000000 --- a/spec/presenters/account_relationships_presenter_spec.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AccountRelationshipsPresenter do - describe '.initialize' do - before do - allow(Account).to receive(:following_map).with(accounts.pluck(:id), current_account_id).and_return(default_map) - allow(Account).to receive(:followed_by_map).with(accounts.pluck(:id), current_account_id).and_return(default_map) - allow(Account).to receive(:blocking_map).with(accounts.pluck(:id), current_account_id).and_return(default_map) - allow(Account).to receive(:muting_map).with(accounts.pluck(:id), current_account_id).and_return(default_map) - allow(Account).to receive(:requested_map).with(accounts.pluck(:id), current_account_id).and_return(default_map) - allow(Account).to receive(:requested_by_map).with(accounts.pluck(:id), current_account_id).and_return(default_map) - end - - let(:presenter) { described_class.new(accounts, current_account_id, **options) } - let(:current_account_id) { Fabricate(:account).id } - let(:accounts) { [Fabricate(:account)] } - let(:default_map) { { accounts[0].id => true } } - - context 'when options are not set' do - let(:options) { {} } - - it 'sets default maps' do - expect(presenter).to have_attributes( - following: default_map, - followed_by: default_map, - blocking: default_map, - muting: default_map, - requested: default_map, - domain_blocking: { accounts[0].id => nil } - ) - end - end - - context 'with a warm cache' do - let(:options) { {} } - - before do - described_class.new(accounts, current_account_id, **options) - - allow(Account).to receive(:following_map).with([], current_account_id).and_return({}) - allow(Account).to receive(:followed_by_map).with([], current_account_id).and_return({}) - allow(Account).to receive(:blocking_map).with([], current_account_id).and_return({}) - allow(Account).to receive(:muting_map).with([], current_account_id).and_return({}) - allow(Account).to receive(:requested_map).with([], current_account_id).and_return({}) - allow(Account).to receive(:requested_by_map).with([], current_account_id).and_return({}) - end - - it 'sets returns expected values' do - expect(presenter).to have_attributes( - following: default_map, - followed_by: default_map, - blocking: default_map, - muting: default_map, - requested: default_map, - domain_blocking: { accounts[0].id => nil } - ) - end - end - - context 'when options[:following_map] is set' do - let(:options) { { following_map: { 2 => true } } } - - it 'sets @following merged with default_map and options[:following_map]' do - expect(presenter.following).to eq default_map.merge(options[:following_map]) - end - end - - context 'when options[:followed_by_map] is set' do - let(:options) { { followed_by_map: { 3 => true } } } - - it 'sets @followed_by merged with default_map and options[:followed_by_map]' do - expect(presenter.followed_by).to eq default_map.merge(options[:followed_by_map]) - end - end - - context 'when options[:blocking_map] is set' do - let(:options) { { blocking_map: { 4 => true } } } - - it 'sets @blocking merged with default_map and options[:blocking_map]' do - expect(presenter.blocking).to eq default_map.merge(options[:blocking_map]) - end - end - - context 'when options[:muting_map] is set' do - let(:options) { { muting_map: { 5 => true } } } - - it 'sets @muting merged with default_map and options[:muting_map]' do - expect(presenter.muting).to eq default_map.merge(options[:muting_map]) - end - end - - context 'when options[:requested_map] is set' do - let(:options) { { requested_map: { 6 => true } } } - - it 'sets @requested merged with default_map and options[:requested_map]' do - expect(presenter.requested).to eq default_map.merge(options[:requested_map]) - end - end - - context 'when options[:requested_by_map] is set' do - let(:options) { { requested_by_map: { 6 => true } } } - - it 'sets @requested merged with default_map and options[:requested_by_map]' do - expect(presenter.requested_by).to eq default_map.merge(options[:requested_by_map]) - end - end - - context 'when options[:domain_blocking_map] is set' do - let(:options) { { domain_blocking_map: { 7 => true } } } - - it 'sets @domain_blocking merged with default_map and options[:domain_blocking_map]' do - expect(presenter.domain_blocking).to eq({ accounts[0].id => nil }.merge(options[:domain_blocking_map])) - end - end - end -end diff --git a/spec/presenters/familiar_followers_presenter_spec.rb b/spec/presenters/familiar_followers_presenter_spec.rb deleted file mode 100644 index 853babb84b..0000000000 --- a/spec/presenters/familiar_followers_presenter_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe FamiliarFollowersPresenter do - describe '#accounts' do - subject { described_class.new(requested_accounts, account.id) } - - let(:account) { Fabricate(:account) } - let(:familiar_follower) { Fabricate(:account) } - let(:requested_accounts) { Fabricate.times(2, :account) } - - before do - familiar_follower.follow!(requested_accounts.first) - account.follow!(familiar_follower) - end - - it 'returns a result for each requested account' do - expect(subject.accounts.map(&:id)).to eq requested_accounts.map(&:id) - end - - it 'returns followers you follow' do - result = subject.accounts.first - - expect(result) - .to be_present - .and have_attributes( - id: requested_accounts.first.id, - accounts: contain_exactly(familiar_follower) - ) - end - - context 'when requested account hides followers' do - before do - requested_accounts.first.update(hide_collections: true) - end - - it 'does not return followers you follow' do - result = subject.accounts.first - - expect(result) - .to be_present - .and have_attributes( - id: requested_accounts.first.id, - accounts: be_empty - ) - end - end - - context 'when familiar follower hides follows' do - before do - familiar_follower.update(hide_collections: true) - end - - it 'does not return followers you follow' do - result = subject.accounts.first - - expect(result) - .to be_present - .and have_attributes( - id: requested_accounts.first.id, - accounts: be_empty - ) - end - end - end -end diff --git a/spec/presenters/instance_presenter_spec.rb b/spec/presenters/instance_presenter_spec.rb deleted file mode 100644 index 0d6a416b3f..0000000000 --- a/spec/presenters/instance_presenter_spec.rb +++ /dev/null @@ -1,106 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe InstancePresenter do - let(:instance_presenter) { described_class.new } - - describe '#description' do - it 'delegates site_description to Setting' do - Setting.site_short_description = 'Site desc' - expect(instance_presenter.description).to eq 'Site desc' - end - end - - describe '#extended_description' do - it 'delegates site_extended_description to Setting' do - Setting.site_extended_description = 'Extended desc' - expect(instance_presenter.extended_description).to eq 'Extended desc' - end - end - - describe '#email' do - it 'delegates contact_email to Setting' do - Setting.site_contact_email = 'admin@example.com' - expect(instance_presenter.contact.email).to eq 'admin@example.com' - end - end - - describe '#account' do - it 'returns the account for the site contact username' do - Setting.site_contact_username = 'aaa' - account = Fabricate(:account, username: 'aaa') - expect(instance_presenter.contact.account).to eq(account) - end - end - - describe '#user_count' do - it 'returns the number of site users' do - Rails.cache.write 'user_count', 123 - - expect(instance_presenter.user_count).to eq(123) - end - end - - describe '#status_count' do - it 'returns the number of local statuses' do - Rails.cache.write 'local_status_count', 234 - - expect(instance_presenter.status_count).to eq(234) - end - end - - describe '#domain_count' do - it 'returns the number of known domains' do - Rails.cache.write 'distinct_domain_count', 345 - - expect(instance_presenter.domain_count).to eq(345) - end - end - - describe '#version' do - it 'returns string' do - expect(instance_presenter.version).to be_a String - end - end - - describe '#source_url' do - context 'with the GITHUB_REPOSITORY env variable set' do - around do |example| - ClimateControl.modify GITHUB_REPOSITORY: 'other/repo' do - example.run - end - end - - it 'uses the env variable to build a repo URL' do - expect(instance_presenter.source_url).to eq('https://github.com/other/repo') - end - end - - context 'without the GITHUB_REPOSITORY env variable set' do - around do |example| - ClimateControl.modify GITHUB_REPOSITORY: nil do - example.run - end - end - - it 'defaults to the core chuckya repo URL' do - expect(instance_presenter.source_url).to eq('https://github.com/TheEssem/mastodon') - end - end - end - - describe '#thumbnail' do - it 'returns SiteUpload' do - thumbnail = Fabricate(:site_upload, var: 'thumbnail') - expect(instance_presenter.thumbnail).to eq(thumbnail) - end - end - - describe '#mascot' do - it 'returns SiteUpload' do - mascot = Fabricate(:site_upload, var: 'mascot') - expect(instance_presenter.mascot).to eq(mascot) - end - end -end diff --git a/spec/presenters/status_relationships_presenter_spec.rb b/spec/presenters/status_relationships_presenter_spec.rb deleted file mode 100644 index af6a93b82b..0000000000 --- a/spec/presenters/status_relationships_presenter_spec.rb +++ /dev/null @@ -1,151 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe StatusRelationshipsPresenter do - describe '.initialize' do - before do - allow(Status).to receive(:reblogs_map).with(match_array(status_ids), current_account_id).and_return(default_map) - allow(Status).to receive(:favourites_map).with(status_ids, current_account_id).and_return(default_map) - allow(Status).to receive(:bookmarks_map).with(status_ids, current_account_id).and_return(default_map) - allow(Status).to receive(:mutes_map).with(anything, current_account_id).and_return(default_map) - allow(Status).to receive(:pins_map).with(anything, current_account_id).and_return(default_map) - end - - let(:presenter) { described_class.new(statuses, current_account_id, **options) } - let(:current_account_id) { Fabricate(:account).id } - let(:statuses) { [Fabricate(:status)] } - let(:status_ids) { statuses.map(&:id) + statuses.filter_map(&:reblog_of_id) } - let(:default_map) { { 1 => true } } - - context 'when options are not set' do - let(:options) { {} } - - it 'sets default maps' do - expect(presenter).to have_attributes( - reblogs_map: eq(default_map), - favourites_map: eq(default_map), - bookmarks_map: eq(default_map), - mutes_map: eq(default_map), - pins_map: eq(default_map) - ) - end - end - - context 'when options[:reblogs_map] is set' do - let(:options) { { reblogs_map: { 2 => true } } } - - it 'sets @reblogs_map merged with default_map and options[:reblogs_map]' do - expect(presenter.reblogs_map).to eq default_map.merge(options[:reblogs_map]) - end - end - - context 'when options[:favourites_map] is set' do - let(:options) { { favourites_map: { 3 => true } } } - - it 'sets @favourites_map merged with default_map and options[:favourites_map]' do - expect(presenter.favourites_map).to eq default_map.merge(options[:favourites_map]) - end - end - - context 'when options[:bookmarks_map] is set' do - let(:options) { { bookmarks_map: { 4 => true } } } - - it 'sets @bookmarks_map merged with default_map and options[:bookmarks_map]' do - expect(presenter.bookmarks_map).to eq default_map.merge(options[:bookmarks_map]) - end - end - - context 'when options[:mutes_map] is set' do - let(:options) { { mutes_map: { 5 => true } } } - - it 'sets @mutes_map merged with default_map and options[:mutes_map]' do - expect(presenter.mutes_map).to eq default_map.merge(options[:mutes_map]) - end - end - - context 'when options[:pins_map] is set' do - let(:options) { { pins_map: { 6 => true } } } - - it 'sets @pins_map merged with default_map and options[:pins_map]' do - expect(presenter.pins_map).to eq default_map.merge(options[:pins_map]) - end - end - - context 'when post includes filtered terms' do - let(:statuses) { [Fabricate(:status, text: 'this toot is about that banned word'), Fabricate(:status, reblog: Fabricate(:status, text: 'this toot is about an irrelevant word'))] } - let(:options) { {} } - - before do - Account.find(current_account_id).custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }]) - end - - it 'sets @filters_map to filter top-level status' do - matched_filters = presenter.filters_map[statuses[0].id] - - expect(matched_filters) - .to be_an(Array) - .and have_attributes(size: 1) - .and contain_exactly( - have_attributes( - filter: have_attributes(title: 'filter1'), - keyword_matches: contain_exactly('banned') - ) - ) - end - - it 'sets @filters_map to filter reblogged status' do - matched_filters = presenter.filters_map[statuses[1].reblog_of_id] - - expect(matched_filters) - .to be_an(Array) - .and have_attributes(size: 1) - .and contain_exactly( - have_attributes( - filter: have_attributes(title: 'filter1'), - keyword_matches: contain_exactly('irrelevant') - ) - ) - end - end - - context 'when post includes filtered individual statuses' do - let(:statuses) { [Fabricate(:status, text: 'hello world'), Fabricate(:status, reblog: Fabricate(:status, text: 'this toot is about an irrelevant word'))] } - let(:options) { {} } - - before do - filter = Account.find(current_account_id).custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide) - filter.statuses.create!(status_id: statuses[0].id) - filter.statuses.create!(status_id: statuses[1].reblog_of_id) - end - - it 'sets @filters_map to filter top-level status' do - matched_filters = presenter.filters_map[statuses[0].id] - - expect(matched_filters) - .to be_an(Array) - .and have_attributes(size: 1) - .and contain_exactly( - have_attributes( - filter: have_attributes(title: 'filter1'), - status_matches: contain_exactly(statuses.first.id) - ) - ) - end - - it 'sets @filters_map to filter reblogged status' do - matched_filters = presenter.filters_map[statuses[1].reblog_of_id] - - expect(matched_filters) - .to be_an(Array) - .and have_attributes(size: 1) - .and contain_exactly( - have_attributes( - filter: have_attributes(title: 'filter1'), - status_matches: contain_exactly(statuses.second.reblog_of_id) - ) - ) - end - end - end -end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb deleted file mode 100644 index d4b9bddf93..0000000000 --- a/spec/rails_helper.rb +++ /dev/null @@ -1,180 +0,0 @@ -# frozen_string_literal: true - -ENV['RAILS_ENV'] ||= 'test' - -unless ENV['DISABLE_SIMPLECOV'] == 'true' - require 'simplecov' - - SimpleCov.start 'rails' do - if ENV['CI'] - require 'simplecov-lcov' - formatter SimpleCov::Formatter::LcovFormatter - formatter.config.report_with_single_file = true - else - formatter SimpleCov::Formatter::HTMLFormatter - end - - enable_coverage :branch - - add_filter 'lib/linter' - - add_group 'Libraries', 'lib' - add_group 'Policies', 'app/policies' - add_group 'Presenters', 'app/presenters' - add_group 'Serializers', 'app/serializers' - add_group 'Services', 'app/services' - add_group 'Validators', 'app/validators' - end -end - -# This needs to be defined before Rails is initialized -STREAMING_PORT = ENV.fetch('TEST_STREAMING_PORT', '4020') -ENV['STREAMING_API_BASE_URL'] = "http://localhost:#{STREAMING_PORT}" - -require File.expand_path('../config/environment', __dir__) - -abort('The Rails environment is running in production mode!') if Rails.env.production? - -require 'spec_helper' -require 'rspec/rails' -require 'webmock/rspec' -require 'paperclip/matchers' -require 'capybara/rspec' -require 'chewy/rspec' -require 'email_spec/rspec' -require 'test_prof/recipes/rspec/before_all' - -Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f } - -ActiveRecord::Migration.maintain_test_schema! -WebMock.disable_net_connect!( - allow_localhost: true, - allow: Chewy.settings[:host] -) -Sidekiq.logger = nil - -DatabaseCleaner.strategy = [:deletion] - -Devise::Test::ControllerHelpers.module_eval do - alias_method :original_sign_in, :sign_in - - def sign_in(resource, _deprecated = nil, scope: nil) - original_sign_in(resource, scope: scope) - - SessionActivation.deactivate warden.cookies.signed['_session_id'] - - warden.cookies.signed['_session_id'] = { - value: resource.activate_session(warden.request), - expires: 1.year.from_now, - httponly: true, - } - end -end - -RSpec.configure do |config| - # By default, skip specs that need full JS browser - config.filter_run_excluding :js - - # By default, skip specs that need elastic search server - config.filter_run_excluding :search - - # By default, skip specs that need the streaming server - config.filter_run_excluding :streaming - - config.fixture_paths = [ - Rails.root.join('spec', 'fixtures'), - ] - config.use_transactional_fixtures = true - config.order = 'random' - config.infer_spec_type_from_file_location! - config.filter_rails_from_backtrace! - - # Set type to `cli` for all CLI specs - config.define_derived_metadata(file_path: Regexp.new('spec/lib/mastodon/cli')) do |metadata| - metadata[:type] = :cli - end - - # Set `search` metadata true for all specs in spec/search/ - config.define_derived_metadata(file_path: Regexp.new('spec/search/*')) do |metadata| - metadata[:search] = true - end - - config.include Devise::Test::ControllerHelpers, type: :controller - config.include Devise::Test::ControllerHelpers, type: :helper - config.include Devise::Test::ControllerHelpers, type: :view - config.include Devise::Test::IntegrationHelpers, type: :system - config.include Devise::Test::IntegrationHelpers, type: :request - config.include ActionMailer::TestHelper - config.include Paperclip::Shoulda::Matchers - config.include ActiveSupport::Testing::TimeHelpers - config.include Chewy::Rspec::Helpers - config.include Redisable - config.include ThreadingHelpers - config.include SignedRequestHelpers, type: :request - config.include CommandLineHelpers, type: :cli - - config.around(:each, use_transactional_tests: false) do |example| - self.use_transactional_tests = false - example.run - self.use_transactional_tests = true - end - - config.around do |example| - if example.metadata[:inline_jobs] == true - Sidekiq::Testing.inline! - else - Sidekiq::Testing.fake! - end - example.run - end - - config.before :each, type: :cli do - stub_reset_connection_pools - end - - config.before do |example| - allow(Resolv::DNS).to receive(:open).and_raise('Real DNS queries are disabled, stub Resolv::DNS as needed') unless example.metadata[:type] == :system - end - - config.before do |example| - unless example.metadata[:attachment_processing] - allow_any_instance_of(Paperclip::Attachment).to receive(:post_process).and_return(true) # rubocop:disable RSpec/AnyInstance - end - end - - config.after do - Rails.cache.clear - redis.del(redis.keys) - end - - # Assign types based on dir name for non-inferred types - config.define_derived_metadata(file_path: %r{/spec/}) do |metadata| - unless metadata.key?(:type) - match = metadata[:location].match(%r{/spec/([^/]+)/}) - metadata[:type] = match[1].singularize.to_sym - end - end -end - -RSpec::Sidekiq.configure do |config| - config.warn_when_jobs_not_processed_by_sidekiq = false -end - -RSpec::Matchers.define_negated_matcher :not_change, :change -RSpec::Matchers.define_negated_matcher :not_eq, :eq -RSpec::Matchers.define_negated_matcher :not_include, :include - -def request_fixture(name) - Rails.root.join('spec', 'fixtures', 'requests', name).read -end - -def attachment_fixture(name) - Rails.root.join('spec', 'fixtures', 'files', name).open -end - -def stub_reset_connection_pools - # TODO: Is there a better way to correctly run specs without stubbing this? - # (Avoids reset_connection_pools! in test env) - allow(ActiveRecord::Base).to receive(:establish_connection) - allow(RedisConfiguration).to receive(:establish_pool) -end diff --git a/spec/requests/account_show_page_spec.rb b/spec/requests/account_show_page_spec.rb deleted file mode 100644 index 830d778608..0000000000 --- a/spec/requests/account_show_page_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'The account show page' do - it 'has valid opengraph tags' do - alice = Fabricate(:account, username: 'alice', display_name: 'Alice') - _status = Fabricate(:status, account: alice, text: 'Hello World') - - get '/@alice' - - expect(head_link_icons.size).to eq(3) # Three favicons with sizes - - expect(head_meta_content('og:title')).to match alice.display_name - expect(head_meta_content('og:type')).to eq 'profile' - expect(head_meta_content('og:image')).to match '.+' - expect(head_meta_content('og:url')).to match 'http://.+' - end - - def head_link_icons - head_section.css('link[rel=icon]') - end - - def head_meta_content(property) - head_section.meta("[@property='#{property}']")[:content] - end - - def head_section - Nokogiri::Slop(response.body).html.head - end -end diff --git a/spec/requests/accounts_spec.rb b/spec/requests/accounts_spec.rb deleted file mode 100644 index bf067cdc38..0000000000 --- a/spec/requests/accounts_spec.rb +++ /dev/null @@ -1,301 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Accounts show response' do - let(:account) { Fabricate(:account) } - - context 'with an unapproved account' do - before { account.user.update(approved: false) } - - it 'returns http not found' do - %w(html json rss).each do |format| - get short_account_path(username: account.username), as: format - - expect(response).to have_http_status(404) - end - end - end - - context 'with a permanently suspended account' do - before do - account.suspend! - account.deletion_request.destroy - end - - it 'returns http gone' do - %w(html json rss).each do |format| - get short_account_path(username: account.username), as: format - - expect(response).to have_http_status(410) - end - end - end - - context 'with a temporarily suspended account' do - before { account.suspend! } - - it 'returns appropriate http response code' do - { html: 403, json: 200, rss: 403 }.each do |format, code| - get short_account_path(username: account.username), as: format - - expect(response).to have_http_status(code) - end - end - end - - describe 'GET to short username paths' do - context 'with existing statuses' do - let!(:status) { Fabricate(:status, account: account) } - let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) } - let!(:status_self_reply) { Fabricate(:status, account: account, thread: status) } - let!(:status_media) { Fabricate(:status, account: account) } - let!(:status_pinned) { Fabricate(:status, account: account) } - let!(:status_private) { Fabricate(:status, account: account, visibility: :private) } - let!(:status_direct) { Fabricate(:status, account: account, visibility: :direct) } - let!(:status_reblog) { Fabricate(:status, account: account, reblog: Fabricate(:status)) } - - before do - status_media.media_attachments << Fabricate(:media_attachment, account: account, type: :image) - account.pinned_statuses << status_pinned - account.pinned_statuses << status_private - end - - context 'with HTML' do - let(:format) { 'html' } - - shared_examples 'common HTML response' do - it 'returns a standard HTML response', :aggregate_failures do - expect(response) - .to have_http_status(200) - .and render_template(:show) - - expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account) - end - end - - context 'with a normal account in an HTML request' do - before do - get short_account_path(username: account.username), as: format - end - - it_behaves_like 'common HTML response' - end - - context 'with replies' do - before do - get short_account_with_replies_path(username: account.username), as: format - end - - it_behaves_like 'common HTML response' - end - - context 'with media' do - before do - get short_account_media_path(username: account.username), as: format - end - - it_behaves_like 'common HTML response' - end - - context 'with tag' do - let(:tag) { Fabricate(:tag) } - - let!(:status_tag) { Fabricate(:status, account: account) } - - before do - status_tag.tags << tag - get short_account_tag_path(username: account.username, tag: tag), as: format - end - - it_behaves_like 'common HTML response' - end - end - - context 'with JSON' do - let(:authorized_fetch_mode) { false } - let(:headers) { { 'ACCEPT' => 'application/json' } } - - around do |example| - ClimateControl.modify AUTHORIZED_FETCH: authorized_fetch_mode.to_s do - example.run - end - end - - context 'with a normal account in a JSON request' do - before do - get short_account_path(username: account.username), headers: headers - end - - it 'returns a JSON version of the account', :aggregate_failures do - expect(response) - .to have_http_status(200) - .and have_attributes( - media_type: eq('application/activity+json') - ) - - expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) - end - - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - - context 'with authorized fetch mode' do - let(:authorized_fetch_mode) { true } - - it 'returns http unauthorized' do - expect(response).to have_http_status(401) - end - end - end - - context 'when signed in' do - let(:user) { Fabricate(:user) } - - before do - sign_in(user) - get short_account_path(username: account.username), headers: headers.merge({ 'Cookie' => '123' }) - end - - it 'returns a private JSON version of the account', :aggregate_failures do - expect(response) - .to have_http_status(200) - .and have_attributes( - media_type: eq('application/activity+json') - ) - - expect(response.headers['Cache-Control']).to include 'private' - - expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) - end - end - - context 'with signature' do - let(:remote_account) { Fabricate(:account, domain: 'example.com') } - - before do - get short_account_path(username: account.username), headers: headers, sign_with: remote_account - end - - it 'returns a JSON version of the account', :aggregate_failures do - expect(response) - .to have_http_status(200) - .and have_attributes( - media_type: eq('application/activity+json') - ) - - expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) - end - - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - - context 'with authorized fetch mode' do - let(:authorized_fetch_mode) { true } - - it 'returns a private signature JSON version of the account', :aggregate_failures do - expect(response) - .to have_http_status(200) - .and have_attributes( - media_type: eq('application/activity+json') - ) - - expect(response.headers['Cache-Control']).to include 'private' - expect(response.headers['Vary']).to include 'Signature' - - expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) - end - end - end - end - - context 'with RSS' do - let(:format) { 'rss' } - - context 'with a normal account in an RSS request' do - before do - get short_account_path(username: account.username, format: format) - end - - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - - it 'responds with correct statuses', :aggregate_failures do - expect(response).to have_http_status(200) - expect(response.body).to include(status_tag_for(status_media)) - expect(response.body).to include(status_tag_for(status_self_reply)) - expect(response.body).to include(status_tag_for(status)) - expect(response.body).to_not include(status_tag_for(status_direct)) - expect(response.body).to_not include(status_tag_for(status_private)) - expect(response.body).to_not include(status_tag_for(status_reblog.reblog)) - expect(response.body).to_not include(status_tag_for(status_reply)) - end - end - - context 'with replies' do - before do - get short_account_with_replies_path(username: account.username, format: format) - end - - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - - it 'responds with correct statuses with replies', :aggregate_failures do - expect(response).to have_http_status(200) - expect(response.body).to include(status_tag_for(status_media)) - expect(response.body).to include(status_tag_for(status_reply)) - expect(response.body).to include(status_tag_for(status_self_reply)) - expect(response.body).to include(status_tag_for(status)) - expect(response.body).to_not include(status_tag_for(status_direct)) - expect(response.body).to_not include(status_tag_for(status_private)) - expect(response.body).to_not include(status_tag_for(status_reblog.reblog)) - end - end - - context 'with media' do - before do - get short_account_media_path(username: account.username, format: format) - end - - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - - it 'responds with correct statuses with media', :aggregate_failures do - expect(response).to have_http_status(200) - expect(response.body).to include(status_tag_for(status_media)) - expect(response.body).to_not include(status_tag_for(status_direct)) - expect(response.body).to_not include(status_tag_for(status_private)) - expect(response.body).to_not include(status_tag_for(status_reblog.reblog)) - expect(response.body).to_not include(status_tag_for(status_reply)) - expect(response.body).to_not include(status_tag_for(status_self_reply)) - expect(response.body).to_not include(status_tag_for(status)) - end - end - - context 'with tag' do - let(:tag) { Fabricate(:tag) } - - let!(:status_tag) { Fabricate(:status, account: account) } - - before do - status_tag.tags << tag - get short_account_tag_path(username: account.username, tag: tag, format: format) - end - - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - - it 'responds with correct statuses with a tag', :aggregate_failures do - expect(response).to have_http_status(200) - expect(response.body).to include(status_tag_for(status_tag)) - expect(response.body).to_not include(status_tag_for(status_direct)) - expect(response.body).to_not include(status_tag_for(status_media)) - expect(response.body).to_not include(status_tag_for(status_private)) - expect(response.body).to_not include(status_tag_for(status_reblog.reblog)) - expect(response.body).to_not include(status_tag_for(status_reply)) - expect(response.body).to_not include(status_tag_for(status_self_reply)) - expect(response.body).to_not include(status_tag_for(status)) - end - end - end - end - end - - def status_tag_for(status) - ActivityPub::TagManager.instance.url_for(status) - end -end diff --git a/spec/requests/anonymous_cookies_spec.rb b/spec/requests/anonymous_cookies_spec.rb deleted file mode 100644 index 427f54e449..0000000000 --- a/spec/requests/anonymous_cookies_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -context 'when visited anonymously' do - around do |example| - old = ActionController::Base.allow_forgery_protection - ActionController::Base.allow_forgery_protection = true - - example.run - - ActionController::Base.allow_forgery_protection = old - end - - describe 'account pages' do - it 'do not set cookies' do - alice = Fabricate(:account, username: 'alice', display_name: 'Alice') - _status = Fabricate(:status, account: alice, text: 'Hello World') - - get '/@alice' - - expect(response.cookies).to be_empty - end - end - - describe 'status pages' do - it 'do not set cookies' do - alice = Fabricate(:account, username: 'alice', display_name: 'Alice') - status = Fabricate(:status, account: alice, text: 'Hello World') - - get short_account_status_url(alice, status) - - expect(response.cookies).to be_empty - end - end - - describe 'the /about page' do - it 'does not set cookies' do - get '/about' - - expect(response.cookies).to be_empty - end - end -end diff --git a/spec/requests/api/v1/accounts/credentials_spec.rb b/spec/requests/api/v1/accounts/credentials_spec.rb deleted file mode 100644 index ce5940d468..0000000000 --- a/spec/requests/api/v1/accounts/credentials_spec.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'credentials API' do - let(:user) { Fabricate(:user, account_attributes: { discoverable: false, locked: true, indexable: false }) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:accounts write:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/accounts/verify_credentials' do - subject do - get '/api/v1/accounts/verify_credentials', headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'write write:accounts' - - it 'returns http success with expected content' do - subject - - expect(response) - .to have_http_status(200) - expect(body_as_json).to include({ - source: hash_including({ - discoverable: false, - indexable: false, - }), - locked: true, - }) - end - - describe 'allows the profile scope' do - let(:scopes) { 'profile' } - - it 'returns the response successfully' do - subject - - expect(response).to have_http_status(200) - - expect(body_as_json).to include({ - locked: true, - }) - end - end - end - - describe 'PATCH /api/v1/accounts/update_credentials' do - subject do - patch '/api/v1/accounts/update_credentials', headers: headers, params: params - end - - before { allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) } - - let(:params) do - { - avatar: fixture_file_upload('avatar.gif', 'image/gif'), - discoverable: true, - display_name: "Alice Isn't Dead", - header: fixture_file_upload('attachment.jpg', 'image/jpeg'), - indexable: true, - locked: false, - note: 'Hello!', - source: { - privacy: 'unlisted', - sensitive: true, - }, - } - end - - it_behaves_like 'forbidden for wrong scope', 'read read:accounts' - - describe 'with empty source list' do - let(:params) { { display_name: "I'm a cat", source: {} } } - - it 'returns http success' do - subject - expect(response).to have_http_status(200) - end - end - - describe 'with invalid data' do - let(:params) { { note: "This is too long. #{'a' * Account::NOTE_LENGTH_LIMIT}" } } - - it 'returns http unprocessable entity' do - subject - expect(response).to have_http_status(422) - end - end - - it 'returns http success with updated JSON attributes' do - subject - - expect(response) - .to have_http_status(200) - - expect(body_as_json).to include({ - source: hash_including({ - discoverable: true, - indexable: true, - }), - locked: false, - }) - - expect(ActivityPub::UpdateDistributionWorker) - .to have_received(:perform_async).with(user.account_id) - end - - def expect_account_updates - expect(user.account.reload) - .to have_attributes( - display_name: eq("Alice Isn't Dead"), - note: 'Hello!', - avatar: exist, - header: exist - ) - end - - def expect_user_updates - expect(user.reload) - .to have_attributes( - setting_default_privacy: eq('unlisted'), - setting_default_sensitive: be(true) - ) - end - end -end diff --git a/spec/requests/api/v1/accounts/familiar_followers_spec.rb b/spec/requests/api/v1/accounts/familiar_followers_spec.rb deleted file mode 100644 index fdc0a3a932..0000000000 --- a/spec/requests/api/v1/accounts/familiar_followers_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Accounts Familiar Followers API' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:follows' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account) } - - describe 'GET /api/v1/accounts/familiar_followers' do - it 'returns http success' do - get '/api/v1/accounts/familiar_followers', params: { account_id: account.id, limit: 2 }, headers: headers - - expect(response).to have_http_status(200) - end - - context 'when there are duplicate account IDs in the params' do - let(:account_a) { Fabricate(:account) } - let(:account_b) { Fabricate(:account) } - - it 'removes duplicate account IDs from params' do - account_ids = [account_a, account_b, account_b, account_a, account_a].map { |a| a.id.to_s } - get '/api/v1/accounts/familiar_followers', params: { id: account_ids }, headers: headers - - expect(body_as_json.pluck(:id)).to contain_exactly(account_a.id.to_s, account_b.id.to_s) - end - end - end -end diff --git a/spec/requests/api/v1/accounts/featured_tags_spec.rb b/spec/requests/api/v1/accounts/featured_tags_spec.rb deleted file mode 100644 index bae7d448b6..0000000000 --- a/spec/requests/api/v1/accounts/featured_tags_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'account featured tags API' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account) } - - describe 'GET /api/v1/accounts/:id/featured_tags' do - subject do - get "/api/v1/accounts/#{account.id}/featured_tags", headers: headers - end - - before do - account.featured_tags.create!(name: 'foo') - account.featured_tags.create!(name: 'bar') - end - - it 'returns the expected tags', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to contain_exactly(a_hash_including({ - name: 'bar', - url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/bar", - }), a_hash_including({ - name: 'foo', - url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/foo", - })) - end - - context 'when the account is remote' do - it 'returns the expected tags', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to contain_exactly(a_hash_including({ - name: 'bar', - url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/bar", - }), a_hash_including({ - name: 'foo', - url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/foo", - })) - end - end - end -end diff --git a/spec/requests/api/v1/accounts/follower_accounts_spec.rb b/spec/requests/api/v1/accounts/follower_accounts_spec.rb deleted file mode 100644 index 7ff92d6a48..0000000000 --- a/spec/requests/api/v1/accounts/follower_accounts_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Accounts FollowerAccounts' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account) } - let(:alice) { Fabricate(:account) } - let(:bob) { Fabricate(:account) } - - before do - alice.follow!(account) - bob.follow!(account) - end - - describe 'GET /api/v1/accounts/:acount_id/followers' do - it 'returns accounts following the given account', :aggregate_failures do - get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq 2 - expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s) - end - - it 'does not return blocked users', :aggregate_failures do - user.account.block!(bob) - get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq 1 - expect(body_as_json[0][:id]).to eq alice.id.to_s - end - - context 'when requesting user is blocked' do - before do - account.block!(user.account) - end - - it 'hides results' do - get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers - expect(body_as_json.size).to eq 0 - end - end - - context 'when requesting user is the account owner' do - let(:user) { account.user } - - it 'returns all accounts, including muted accounts' do - account.mute!(bob) - get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers - - expect(body_as_json.size).to eq 2 - expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s) - end - end - end -end diff --git a/spec/requests/api/v1/accounts/following_accounts_spec.rb b/spec/requests/api/v1/accounts/following_accounts_spec.rb deleted file mode 100644 index b343a48654..0000000000 --- a/spec/requests/api/v1/accounts/following_accounts_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Accounts FollowingAccounts' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account) } - let(:alice) { Fabricate(:account) } - let(:bob) { Fabricate(:account) } - - before do - account.follow!(alice) - account.follow!(bob) - end - - describe 'GET /api/v1/accounts/:account_id/following' do - it 'returns accounts followed by the given account', :aggregate_failures do - get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq 2 - expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s) - end - - it 'does not return blocked users', :aggregate_failures do - user.account.block!(bob) - get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq 1 - expect(body_as_json[0][:id]).to eq alice.id.to_s - end - - context 'when requesting user is blocked' do - before do - account.block!(user.account) - end - - it 'hides results' do - get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers - expect(body_as_json.size).to eq 0 - end - end - - context 'when requesting user is the account owner' do - let(:user) { account.user } - - it 'returns all accounts, including muted accounts' do - account.mute!(bob) - get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers - - expect(body_as_json.size).to eq 2 - expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s) - end - end - end -end diff --git a/spec/requests/api/v1/accounts/identity_proofs_spec.rb b/spec/requests/api/v1/accounts/identity_proofs_spec.rb deleted file mode 100644 index 3727af7e89..0000000000 --- a/spec/requests/api/v1/accounts/identity_proofs_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Accounts Identity Proofs API' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account) } - - describe 'GET /api/v1/accounts/identity_proofs' do - it 'returns http success' do - get "/api/v1/accounts/#{account.id}/identity_proofs", params: { limit: 2 }, headers: headers - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/v1/accounts/lists_spec.rb b/spec/requests/api/v1/accounts/lists_spec.rb deleted file mode 100644 index 48c0337e54..0000000000 --- a/spec/requests/api/v1/accounts/lists_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Accounts Lists API' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:lists' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account) } - let(:list) { Fabricate(:list, account: user.account) } - - before do - user.account.follow!(account) - list.accounts << account - end - - describe 'GET /api/v1/accounts/lists' do - it 'returns http success' do - get "/api/v1/accounts/#{account.id}/lists", headers: headers - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/v1/accounts/lookup_spec.rb b/spec/requests/api/v1/accounts/lookup_spec.rb deleted file mode 100644 index 4c022c7c13..0000000000 --- a/spec/requests/api/v1/accounts/lookup_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Accounts Lookup API' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account) } - - describe 'GET /api/v1/accounts/lookup' do - it 'returns http success' do - get '/api/v1/accounts/lookup', params: { account_id: account.id, acct: account.acct }, headers: headers - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/v1/accounts/notes_spec.rb b/spec/requests/api/v1/accounts/notes_spec.rb deleted file mode 100644 index 4f3ac68c74..0000000000 --- a/spec/requests/api/v1/accounts/notes_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Accounts Notes API' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'write:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account) } - let(:comment) { 'foo' } - - describe 'POST /api/v1/accounts/:account_id/note' do - subject do - post "/api/v1/accounts/#{account.id}/note", params: { comment: comment }, headers: headers - end - - context 'when account note has reasonable length', :aggregate_failures do - let(:comment) { 'foo' } - - it 'updates account note' do - subject - - expect(response).to have_http_status(200) - expect(AccountNote.find_by(account_id: user.account.id, target_account_id: account.id).comment).to eq comment - end - end - - context 'when account note exceeds allowed length', :aggregate_failures do - let(:comment) { 'a' * 2_001 } - - it 'does not create account note' do - subject - - expect(response).to have_http_status(422) - expect(AccountNote.where(account_id: user.account.id, target_account_id: account.id)).to_not exist - end - end - end -end diff --git a/spec/requests/api/v1/accounts/pins_spec.rb b/spec/requests/api/v1/accounts/pins_spec.rb deleted file mode 100644 index c293715f7e..0000000000 --- a/spec/requests/api/v1/accounts/pins_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Accounts Pins API' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'write:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:kevin) { Fabricate(:user) } - - before do - kevin.account.followers << user.account - end - - describe 'POST /api/v1/accounts/:account_id/pin' do - subject { post "/api/v1/accounts/#{kevin.account.id}/pin", headers: headers } - - it 'creates account_pin', :aggregate_failures do - expect do - subject - end.to change { AccountPin.where(account: user.account, target_account: kevin.account).count }.by(1) - expect(response).to have_http_status(200) - end - end - - describe 'POST /api/v1/accounts/:account_id/unpin' do - subject { post "/api/v1/accounts/#{kevin.account.id}/unpin", headers: headers } - - before do - Fabricate(:account_pin, account: user.account, target_account: kevin.account) - end - - it 'destroys account_pin', :aggregate_failures do - expect do - subject - end.to change { AccountPin.where(account: user.account, target_account: kevin.account).count }.by(-1) - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/v1/accounts/relationships_spec.rb b/spec/requests/api/v1/accounts/relationships_spec.rb deleted file mode 100644 index b06ce0509d..0000000000 --- a/spec/requests/api/v1/accounts/relationships_spec.rb +++ /dev/null @@ -1,185 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'GET /api/v1/accounts/relationships' do - subject do - get '/api/v1/accounts/relationships', headers: headers, params: params - end - - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:follows' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - let(:simon) { Fabricate(:account) } - let(:lewis) { Fabricate(:account) } - let(:bob) { Fabricate(:account, suspended: true) } - - before do - user.account.follow!(simon) - lewis.follow!(user.account) - end - - context 'when provided only one ID' do - let(:params) { { id: simon.id } } - - it 'returns JSON with correct data', :aggregate_failures do - subject - - expect(response) - .to have_http_status(200) - expect(body_as_json) - .to be_an(Enumerable) - .and contain_exactly( - include( - following: true, - followed_by: false - ) - ) - end - end - - context 'when provided multiple IDs' do - let(:params) { { id: [simon.id, lewis.id, bob.id] } } - - context 'when there is returned JSON data' do - context 'with default parameters' do - it 'returns an enumerable json with correct elements, excluding suspended accounts', :aggregate_failures do - subject - - expect(response) - .to have_http_status(200) - expect(body_as_json) - .to be_an(Enumerable) - .and have_attributes( - size: 2 - ) - .and contain_exactly( - include(simon_item), - include(lewis_item) - ) - end - end - - context 'with `with_suspended` parameter' do - let(:params) { { id: [simon.id, lewis.id, bob.id], with_suspended: true } } - - it 'returns an enumerable json with correct elements, including suspended accounts', :aggregate_failures do - subject - - expect(response) - .to have_http_status(200) - expect(body_as_json) - .to be_an(Enumerable) - .and have_attributes( - size: 3 - ) - .and contain_exactly( - include(simon_item), - include(lewis_item), - include(bob_item) - ) - end - end - - context 'when there are duplicate IDs in the params' do - let(:params) { { id: [simon.id, lewis.id, lewis.id, lewis.id, simon.id] } } - - it 'removes duplicate account IDs from params' do - subject - - expect(body_as_json) - .to be_an(Enumerable) - .and have_attributes( - size: 2 - ) - .and contain_exactly( - include(simon_item), - include(lewis_item) - ) - end - end - - def simon_item - { - id: simon.id.to_s, - following: true, - showing_reblogs: true, - followed_by: false, - muting: false, - requested: false, - domain_blocking: false, - } - end - - def lewis_item - { - id: lewis.id.to_s, - following: false, - showing_reblogs: false, - followed_by: true, - muting: false, - requested: false, - domain_blocking: false, - } - end - - def bob_item - { - id: bob.id.to_s, - following: false, - showing_reblogs: false, - followed_by: false, - muting: false, - requested: false, - domain_blocking: false, - } - end - end - - it 'returns JSON with correct data on previously cached requests' do - # Initial request including multiple accounts in params - get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id, lewis.id] } - expect(body_as_json) - .to have_attributes(size: 2) - - # Subsequent request with different id, should override cache from first request - get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id] } - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_an(Enumerable) - .and have_attributes( - size: 1 - ) - .and contain_exactly( - include( - following: true, - showing_reblogs: true - ) - ) - end - - it 'returns JSON with correct data after change too' do - subject - user.account.unfollow!(simon) - - get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id] } - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_an(Enumerable) - .and contain_exactly( - include( - following: false, - showing_reblogs: false - ) - ) - end - end -end diff --git a/spec/requests/api/v1/accounts/search_spec.rb b/spec/requests/api/v1/accounts/search_spec.rb deleted file mode 100644 index 76b32e7b2c..0000000000 --- a/spec/requests/api/v1/accounts/search_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Accounts Search API' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/accounts/search' do - it 'returns http success' do - get '/api/v1/accounts/search', params: { q: 'query' }, headers: headers - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/v1/accounts/statuses_spec.rb b/spec/requests/api/v1/accounts/statuses_spec.rb deleted file mode 100644 index 97cdbe0156..0000000000 --- a/spec/requests/api/v1/accounts/statuses_spec.rb +++ /dev/null @@ -1,129 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Accounts Statuses' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:statuses' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/accounts/:account_id/statuses' do - it 'returns expected headers', :aggregate_failures do - status = Fabricate(:status, account: user.account) - get "/api/v1/accounts/#{user.account.id}/statuses", params: { limit: 1 }, headers: headers - - expect(response) - .to have_http_status(200) - .and include_pagination_headers( - prev: api_v1_account_statuses_url(limit: 1, min_id: status.id), - next: api_v1_account_statuses_url(limit: 1, max_id: status.id) - ) - end - - context 'with only media' do - it 'returns http success' do - get "/api/v1/accounts/#{user.account.id}/statuses", params: { only_media: true }, headers: headers - - expect(response).to have_http_status(200) - end - end - - context 'with exclude replies' do - let!(:status) { Fabricate(:status, account: user.account) } - let!(:status_self_reply) { Fabricate(:status, account: user.account, thread: status) } - - before do - Fabricate(:status, account: user.account, thread: Fabricate(:status)) # Reply to another user - get "/api/v1/accounts/#{user.account.id}/statuses", params: { exclude_replies: true }, headers: headers - end - - it 'returns posts along with self replies', :aggregate_failures do - expect(response) - .to have_http_status(200) - expect(body_as_json) - .to have_attributes(size: 2) - .and contain_exactly( - include(id: status.id.to_s), - include(id: status_self_reply.id.to_s) - ) - end - end - - context 'with only own pinned' do - before do - Fabricate(:status_pin, account: user.account, status: Fabricate(:status, account: user.account)) - end - - it 'returns http success and includes a header link' do - get "/api/v1/accounts/#{user.account.id}/statuses", params: { pinned: true }, headers: headers - - expect(response) - .to have_http_status(200) - .and include_pagination_headers(prev: api_v1_account_statuses_url(pinned: true, min_id: Status.first.id)) - end - end - - context 'with enough pinned statuses to paginate' do - before do - stub_const 'Api::BaseController::DEFAULT_STATUSES_LIMIT', 1 - 2.times { Fabricate(:status_pin, account: user.account) } - end - - it 'returns http success and header pagination links to prev and next' do - get "/api/v1/accounts/#{user.account.id}/statuses", params: { pinned: true }, headers: headers - - expect(response) - .to have_http_status(200) - .and include_pagination_headers( - prev: api_v1_account_statuses_url(pinned: true, min_id: Status.first.id), - next: api_v1_account_statuses_url(pinned: true, max_id: Status.first.id) - ) - end - end - - context "with someone else's pinned statuses" do - let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com') } - let(:status) { Fabricate(:status, account: account) } - let(:private_status) { Fabricate(:status, account: account, visibility: :private) } - - before do - Fabricate(:status_pin, account: account, status: status) - Fabricate(:status_pin, account: account, status: private_status) - end - - it 'returns http success' do - get "/api/v1/accounts/#{account.id}/statuses", params: { pinned: true }, headers: headers - - expect(response).to have_http_status(200) - end - - context 'when user does not follow account' do - it 'lists the public status only' do - get "/api/v1/accounts/#{account.id}/statuses", params: { pinned: true }, headers: headers - - expect(body_as_json) - .to contain_exactly( - a_hash_including(id: status.id.to_s) - ) - end - end - - context 'when user follows account' do - before do - user.account.follow!(account) - end - - it 'lists both the public and the private statuses' do - get "/api/v1/accounts/#{account.id}/statuses", params: { pinned: true }, headers: headers - - expect(body_as_json) - .to contain_exactly( - a_hash_including(id: status.id.to_s), - a_hash_including(id: private_status.id.to_s) - ) - end - end - end - end -end diff --git a/spec/requests/api/v1/accounts_spec.rb b/spec/requests/api/v1/accounts_spec.rb deleted file mode 100644 index 3d9eb65019..0000000000 --- a/spec/requests/api/v1/accounts_spec.rb +++ /dev/null @@ -1,370 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe '/api/v1/accounts' do - let(:user) { Fabricate(:user) } - let(:scopes) { '' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/accounts?id[]=:id' do - let(:account) { Fabricate(:account) } - let(:other_account) { Fabricate(:account) } - let(:scopes) { 'read:accounts' } - - it 'returns expected response' do - get '/api/v1/accounts', headers: headers, params: { id: [account.id, other_account.id, 123_123] } - - expect(response).to have_http_status(200) - expect(body_as_json).to contain_exactly( - hash_including(id: account.id.to_s), - hash_including(id: other_account.id.to_s) - ) - end - end - - describe 'GET /api/v1/accounts/:id' do - context 'when logged out' do - let(:account) { Fabricate(:account) } - - it 'returns account entity as 200 OK', :aggregate_failures do - get "/api/v1/accounts/#{account.id}" - - expect(response).to have_http_status(200) - expect(body_as_json[:id]).to eq(account.id.to_s) - end - end - - context 'when the account does not exist' do - it 'returns http not found' do - get '/api/v1/accounts/1' - - expect(response).to have_http_status(404) - expect(body_as_json[:error]).to eq('Record not found') - end - end - - context 'when logged in' do - subject do - get "/api/v1/accounts/#{account.id}", headers: headers - end - - let(:account) { Fabricate(:account) } - let(:scopes) { 'read:accounts' } - - it 'returns account entity as 200 OK', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:id]).to eq(account.id.to_s) - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - end - end - - describe 'POST /api/v1/accounts' do - subject do - post '/api/v1/accounts', headers: headers, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement } - end - - let(:client_app) { Fabricate(:application) } - let(:token) { Doorkeeper::AccessToken.find_or_create_for(application: client_app, resource_owner: nil, scopes: 'read write', use_refresh_token: false) } - let(:agreement) { nil } - - context 'when given truthy agreement' do - let(:agreement) { 'true' } - - it 'creates a user', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:access_token]).to_not be_blank - - user = User.find_by(email: 'hello@world.tld') - expect(user).to_not be_nil - expect(user.created_by_application_id).to eq client_app.id - end - end - - context 'when given no agreement' do - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - end - - describe 'POST /api/v1/accounts/:id/follow' do - let(:scopes) { 'write:follows' } - let(:other_account) { Fabricate(:account, username: 'bob', locked: locked) } - - context 'when posting to an other account' do - subject do - post "/api/v1/accounts/#{other_account.id}/follow", headers: headers - end - - context 'with unlocked account' do - let(:locked) { false } - - it 'creates a following relation between user and target user', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - - json = body_as_json - - expect(json[:following]).to be true - expect(json[:requested]).to be false - - expect(user.account.following?(other_account)).to be true - end - - it_behaves_like 'forbidden for wrong scope', 'read:accounts' - end - - context 'with locked account' do - let(:locked) { true } - - it 'creates a follow request relation between user and target user', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - - json = body_as_json - - expect(json[:following]).to be false - expect(json[:requested]).to be true - - expect(user.account.requested?(other_account)).to be true - end - - it_behaves_like 'forbidden for wrong scope', 'read:accounts' - end - end - - context 'when modifying follow options' do - let(:locked) { false } - - before do - user.account.follow!(other_account, reblogs: false, notify: false) - end - - it 'changes reblogs option' do - post "/api/v1/accounts/#{other_account.id}/follow", headers: headers, params: { reblogs: true } - - expect(body_as_json).to include({ - following: true, - showing_reblogs: true, - notifying: false, - }) - end - - it 'changes notify option' do - post "/api/v1/accounts/#{other_account.id}/follow", headers: headers, params: { notify: true } - - expect(body_as_json).to include({ - following: true, - showing_reblogs: false, - notifying: true, - }) - end - - it 'changes languages option' do - post "/api/v1/accounts/#{other_account.id}/follow", headers: headers, params: { languages: %w(en es) } - - expect(body_as_json).to include({ - following: true, - showing_reblogs: false, - notifying: false, - languages: match_array(%w(en es)), - }) - end - end - end - - describe 'POST /api/v1/accounts/:id/unfollow' do - subject do - post "/api/v1/accounts/#{other_account.id}/unfollow", headers: headers - end - - let(:scopes) { 'write:follows' } - let(:other_account) { Fabricate(:account, username: 'bob') } - - before do - user.account.follow!(other_account) - end - - it 'removes the following relation between user and target user', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.following?(other_account)).to be false - end - - it_behaves_like 'forbidden for wrong scope', 'read:accounts' - end - - describe 'POST /api/v1/accounts/:id/remove_from_followers' do - subject do - post "/api/v1/accounts/#{other_account.id}/remove_from_followers", headers: headers - end - - let(:scopes) { 'write:follows' } - let(:other_account) { Fabricate(:account, username: 'bob') } - - before do - other_account.follow!(user.account) - end - - it 'removes the followed relation between user and target user', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.followed_by?(other_account)).to be false - end - - it_behaves_like 'forbidden for wrong scope', 'read:accounts' - end - - describe 'POST /api/v1/accounts/:id/block' do - subject do - post "/api/v1/accounts/#{other_account.id}/block", headers: headers - end - - let(:scopes) { 'write:blocks' } - let(:other_account) { Fabricate(:account, username: 'bob') } - - before do - user.account.follow!(other_account) - end - - it 'creates a blocking relation', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.following?(other_account)).to be false - expect(user.account.blocking?(other_account)).to be true - end - - it_behaves_like 'forbidden for wrong scope', 'read:accounts' - end - - describe 'POST /api/v1/accounts/:id/unblock' do - subject do - post "/api/v1/accounts/#{other_account.id}/unblock", headers: headers - end - - let(:scopes) { 'write:blocks' } - let(:other_account) { Fabricate(:account, username: 'bob') } - - before do - user.account.block!(other_account) - end - - it 'removes the blocking relation between user and target user', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.blocking?(other_account)).to be false - end - - it_behaves_like 'forbidden for wrong scope', 'read:accounts' - end - - describe 'POST /api/v1/accounts/:id/mute' do - subject do - post "/api/v1/accounts/#{other_account.id}/mute", headers: headers - end - - let(:scopes) { 'write:mutes' } - let(:other_account) { Fabricate(:account, username: 'bob') } - - before do - user.account.follow!(other_account) - end - - it 'mutes notifications', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.following?(other_account)).to be true - expect(user.account.muting?(other_account)).to be true - expect(user.account.muting_notifications?(other_account)).to be true - end - - it_behaves_like 'forbidden for wrong scope', 'read:accounts' - end - - describe 'POST /api/v1/accounts/:id/mute with notifications set to false' do - subject do - post "/api/v1/accounts/#{other_account.id}/mute", headers: headers, params: { notifications: false } - end - - let(:scopes) { 'write:mutes' } - let(:other_account) { Fabricate(:account, username: 'bob') } - - before do - user.account.follow!(other_account) - end - - it 'does not mute notifications', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.following?(other_account)).to be true - expect(user.account.muting?(other_account)).to be true - expect(user.account.muting_notifications?(other_account)).to be false - end - - it_behaves_like 'forbidden for wrong scope', 'read:accounts' - end - - describe 'POST /api/v1/accounts/:id/mute with nonzero duration set' do - subject do - post "/api/v1/accounts/#{other_account.id}/mute", headers: headers, params: { duration: 300 } - end - - let(:scopes) { 'write:mutes' } - let(:other_account) { Fabricate(:account, username: 'bob') } - - before do - user.account.follow!(other_account) - end - - it 'mutes notifications', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.following?(other_account)).to be true - expect(user.account.muting?(other_account)).to be true - expect(user.account.muting_notifications?(other_account)).to be true - end - - it_behaves_like 'forbidden for wrong scope', 'read:accounts' - end - - describe 'POST /api/v1/accounts/:id/unmute' do - subject do - post "/api/v1/accounts/#{other_account.id}/unmute", headers: headers - end - - let(:scopes) { 'write:mutes' } - let(:other_account) { Fabricate(:account, username: 'bob') } - - before do - user.account.mute!(other_account) - end - - it 'removes the muting relation between user and target user', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.muting?(other_account)).to be false - end - - it_behaves_like 'forbidden for wrong scope', 'read:accounts' - end -end diff --git a/spec/requests/api/v1/admin/account_actions_spec.rb b/spec/requests/api/v1/admin/account_actions_spec.rb deleted file mode 100644 index 5bcf809401..0000000000 --- a/spec/requests/api/v1/admin/account_actions_spec.rb +++ /dev/null @@ -1,141 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Account actions' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:write admin:write:accounts' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - shared_examples 'a successful notification delivery' do - it 'notifies the user about the action taken', :inline_jobs do - emails = capture_emails { subject } - - expect(emails.size) - .to eq(1) - - expect(emails.first) - .to have_attributes( - to: contain_exactly(target_account.user.email) - ) - end - end - - shared_examples 'a successful logged action' do |action_type, target_type| - it 'logs action' do - subject - - expect(latest_admin_action_log) - .to be_present - .and have_attributes( - action: eq(action_type), - account_id: eq(user.account_id), - target_id: eq(target_type == :user ? target_account.user.id : target_account.id) - ) - end - - private - - def latest_admin_action_log - Admin::ActionLog.last - end - end - - describe 'POST /api/v1/admin/accounts/:id/action' do - subject do - post "/api/v1/admin/accounts/#{target_account.id}/action", headers: headers, params: params - end - - let(:target_account) { Fabricate(:account) } - - context 'with type of disable' do - let(:params) { { type: 'disable' } } - - it_behaves_like 'forbidden for wrong scope', 'admin:read admin:read:accounts' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'a successful notification delivery' - it_behaves_like 'a successful logged action', :disable, :user - - it 'disables the target account' do - expect { subject }.to change { target_account.reload.user_disabled? }.from(false).to(true) - expect(response).to have_http_status(200) - end - end - - context 'with type of sensitive' do - let(:params) { { type: 'sensitive' } } - - it_behaves_like 'forbidden for wrong scope', 'admin:read admin:read:accounts' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'a successful notification delivery' - it_behaves_like 'a successful logged action', :sensitive, :account - - it 'marks the target account as sensitive' do - expect { subject }.to change { target_account.reload.sensitized? }.from(false).to(true) - expect(response).to have_http_status(200) - end - end - - context 'with type of silence' do - let(:params) { { type: 'silence' } } - - it_behaves_like 'forbidden for wrong scope', 'admin:read admin:read:accounts' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'a successful notification delivery' - it_behaves_like 'a successful logged action', :silence, :account - - it 'marks the target account as silenced' do - expect { subject }.to change { target_account.reload.silenced? }.from(false).to(true) - expect(response).to have_http_status(200) - end - end - - context 'with type of suspend' do - let(:params) { { type: 'suspend' } } - - it_behaves_like 'forbidden for wrong scope', 'admin:read admin:read:accounts' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'a successful notification delivery' - it_behaves_like 'a successful logged action', :suspend, :account - - it 'marks the target account as suspended' do - expect { subject }.to change { target_account.reload.suspended? }.from(false).to(true) - expect(response).to have_http_status(200) - end - end - - context 'with type of none' do - let(:params) { { type: 'none' } } - - it_behaves_like 'a successful notification delivery' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - end - - context 'with no type' do - let(:params) { {} } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'with invalid type' do - let(:params) { { type: 'invalid' } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - end -end diff --git a/spec/requests/api/v1/admin/accounts_spec.rb b/spec/requests/api/v1/admin/accounts_spec.rb deleted file mode 100644 index 1615581f0e..0000000000 --- a/spec/requests/api/v1/admin/accounts_spec.rb +++ /dev/null @@ -1,409 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Accounts' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:read:accounts admin:write:accounts' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/admin/accounts' do - subject do - get '/api/v1/admin/accounts', headers: headers, params: params - end - - shared_examples 'a successful request' do - it 'returns the correct accounts', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.pluck(:id)).to match_array(expected_results.map { |a| a.id.to_s }) - end - end - - let!(:remote_account) { Fabricate(:account, domain: 'example.org') } - let!(:suspended_account) { Fabricate(:account, suspended: true) } - let!(:disabled_account) { Fabricate(:user, disabled: true).account } - let!(:pending_account) { Fabricate(:user, approved: false).account } - let!(:admin_account) { user.account } - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'read read:accounts admin:write admin:write:accounts' - it_behaves_like 'forbidden for wrong role', '' - - context 'when requesting active local staff accounts' do - let(:expected_results) { [admin_account] } - let(:params) { { active: 'true', local: 'true', staff: 'true' } } - - it_behaves_like 'a successful request' - end - - context 'when requesting remote accounts from a specified domain' do - let(:expected_results) { [remote_account] } - let(:params) { { by_domain: 'example.org', remote: 'true' } } - - before do - Fabricate(:account, domain: 'foo.bar') - end - - it_behaves_like 'a successful request' - end - - context 'when requesting suspended accounts' do - let(:expected_results) { [suspended_account] } - let(:params) { { suspended: 'true' } } - - before do - Fabricate(:account, domain: 'foo.bar', suspended: true) - end - - it_behaves_like 'a successful request' - end - - context 'when requesting disabled accounts' do - let(:expected_results) { [disabled_account] } - let(:params) { { disabled: 'true' } } - - it_behaves_like 'a successful request' - end - - context 'when requesting pending accounts' do - let(:expected_results) { [pending_account] } - let(:params) { { pending: 'true' } } - - before do - pending_account.user.update(approved: false) - end - - it_behaves_like 'a successful request' - end - - context 'when no parameter is given' do - let(:expected_results) { [disabled_account, pending_account, admin_account] } - - it_behaves_like 'a successful request' - end - - context 'with limit param' do - let(:params) { { limit: 2 } } - - it 'returns only the requested number of accounts', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq(params[:limit]) - end - end - end - - describe 'GET /api/v1/admin/accounts/:id' do - subject do - get "/api/v1/admin/accounts/#{account.id}", headers: headers - end - - let(:account) { Fabricate(:account) } - - it_behaves_like 'forbidden for wrong scope', 'read read:accounts admin:write admin:write:accounts' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns the requested account successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to match( - a_hash_including(id: account.id.to_s, username: account.username, email: account.user.email) - ) - end - - context 'when the account is not found' do - it 'returns http not found' do - get '/api/v1/admin/accounts/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/admin/accounts/:id/approve' do - subject do - post "/api/v1/admin/accounts/#{account.id}/approve", headers: headers - end - - let(:account) { Fabricate(:account) } - - context 'when the account is pending' do - before do - account.user.update(approved: false) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' - it_behaves_like 'forbidden for wrong role', '' - - it 'approves the user successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(account.reload.user_approved?).to be(true) - end - - it 'logs action', :aggregate_failures do - subject - - expect(latest_admin_action_log) - .to be_present - .and have_attributes( - action: eq(:approve), - account_id: eq(user.account_id), - target_id: eq(account.user.id) - ) - end - end - - context 'when the account is already approved' do - it 'returns http forbidden' do - subject - - expect(response).to have_http_status(403) - end - end - - context 'when the account is not found' do - it 'returns http not found' do - post '/api/v1/admin/accounts/-1/approve', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/admin/accounts/:id/reject' do - subject do - post "/api/v1/admin/accounts/#{account.id}/reject", headers: headers - end - - let(:account) { Fabricate(:account) } - - context 'when the account is pending' do - before do - account.user.update(approved: false) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' - it_behaves_like 'forbidden for wrong role', '' - - it 'removes the user successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(User.where(id: account.user.id)).to_not exist - end - - it 'logs action', :aggregate_failures do - subject - - expect(latest_admin_action_log) - .to be_present - .and have_attributes( - action: eq(:reject), - account_id: eq(user.account_id), - target_id: eq(account.user.id) - ) - end - end - - context 'when account is already approved' do - it 'returns http forbidden' do - subject - - expect(response).to have_http_status(403) - end - end - - context 'when the account is not found' do - it 'returns http not found' do - post '/api/v1/admin/accounts/-1/reject', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/admin/accounts/:id/enable' do - subject do - post "/api/v1/admin/accounts/#{account.id}/enable", headers: headers - end - - let(:account) { Fabricate(:account) } - - before do - account.user.update(disabled: true) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' - it_behaves_like 'forbidden for wrong role', '' - - it 'enables the user successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(account.reload.user_disabled?).to be false - end - - context 'when the account is not found' do - it 'returns http not found' do - post '/api/v1/admin/accounts/-1/enable', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/admin/accounts/:id/unsuspend' do - subject do - post "/api/v1/admin/accounts/#{account.id}/unsuspend", headers: headers - end - - let(:account) { Fabricate(:account) } - - context 'when the account is suspended' do - before do - account.suspend! - end - - it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' - it_behaves_like 'forbidden for wrong role', '' - - it 'unsuspends the account successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(account.reload.suspended?).to be false - end - end - - context 'when the account is not suspended' do - it 'returns http forbidden' do - subject - - expect(response).to have_http_status(403) - end - end - - context 'when the account is not found' do - it 'returns http not found' do - post '/api/v1/admin/accounts/-1/unsuspend', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/admin/accounts/:id/unsensitive' do - subject do - post "/api/v1/admin/accounts/#{account.id}/unsensitive", headers: headers - end - - let(:account) { Fabricate(:account) } - - before do - account.update(sensitized_at: 10.days.ago) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' - it_behaves_like 'forbidden for wrong role', '' - - it 'unsensitizes the account successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(account.reload.sensitized?).to be false - end - - context 'when the account is not found' do - it 'returns http not found' do - post '/api/v1/admin/accounts/-1/unsensitive', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/admin/accounts/:id/unsilence' do - subject do - post "/api/v1/admin/accounts/#{account.id}/unsilence", headers: headers - end - - let(:account) { Fabricate(:account) } - - before do - account.update(silenced_at: 3.days.ago) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' - it_behaves_like 'forbidden for wrong role', '' - - it 'unsilences the account successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(account.reload.silenced?).to be false - end - - context 'when the account is not found' do - it 'returns http not found' do - post '/api/v1/admin/accounts/-1/unsilence', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'DELETE /api/v1/admin/accounts/:id' do - subject do - delete "/api/v1/admin/accounts/#{account.id}", headers: headers - end - - let(:account) { Fabricate(:account) } - - context 'when account is suspended' do - before do - account.suspend! - end - - it_behaves_like 'forbidden for wrong scope', 'write write:accounts read admin:read' - it_behaves_like 'forbidden for wrong role', '' - - it 'deletes the account successfully', :aggregate_failures do - allow(Admin::AccountDeletionWorker).to receive(:perform_async) - subject - - expect(response).to have_http_status(200) - expect(Admin::AccountDeletionWorker).to have_received(:perform_async).with(account.id).once - end - end - - context 'when account is not suspended' do - it 'returns http forbidden' do - subject - - expect(response).to have_http_status(403) - end - end - - context 'when the account is not found' do - it 'returns http not found' do - delete '/api/v1/admin/accounts/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - private - - def latest_admin_action_log - Admin::ActionLog.last - end -end diff --git a/spec/requests/api/v1/admin/canonical_email_blocks_spec.rb b/spec/requests/api/v1/admin/canonical_email_blocks_spec.rb deleted file mode 100644 index 3f33b50f39..0000000000 --- a/spec/requests/api/v1/admin/canonical_email_blocks_spec.rb +++ /dev/null @@ -1,250 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Canonical Email Blocks' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'admin:read:canonical_email_blocks admin:write:canonical_email_blocks' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/admin/canonical_email_blocks' do - subject do - get '/api/v1/admin/canonical_email_blocks', headers: headers, params: params - end - - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'read:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - context 'when there is no canonical email block' do - it 'returns an empty list' do - subject - - expect(body_as_json).to be_empty - end - end - - context 'when there are canonical email blocks' do - let!(:canonical_email_blocks) { Fabricate.times(5, :canonical_email_block) } - let(:expected_email_hashes) { canonical_email_blocks.pluck(:canonical_email_hash) } - - it 'returns the correct canonical email hashes' do - subject - - expect(body_as_json.pluck(:canonical_email_hash)).to match_array(expected_email_hashes) - end - - context 'with limit param' do - let(:params) { { limit: 2 } } - - it 'returns only the requested number of canonical email blocks' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - end - - context 'with since_id param' do - let(:params) { { since_id: canonical_email_blocks[1].id } } - - it 'returns only the canonical email blocks after since_id' do - subject - - canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s) - - expect(body_as_json.pluck(:id)).to match_array(canonical_email_blocks_ids[2..]) - end - end - - context 'with max_id param' do - let(:params) { { max_id: canonical_email_blocks[3].id } } - - it 'returns only the canonical email blocks before max_id' do - subject - - canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s) - - expect(body_as_json.pluck(:id)).to match_array(canonical_email_blocks_ids[..2]) - end - end - end - end - - describe 'GET /api/v1/admin/canonical_email_blocks/:id' do - subject do - get "/api/v1/admin/canonical_email_blocks/#{canonical_email_block.id}", headers: headers - end - - let!(:canonical_email_block) { Fabricate(:canonical_email_block) } - - it_behaves_like 'forbidden for wrong scope', 'read:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - context 'when the requested canonical email block exists' do - it 'returns the requested canonical email block data correctly', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - json = body_as_json - - expect(json[:id]).to eq(canonical_email_block.id.to_s) - expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash) - end - end - - context 'when the requested canonical block does not exist' do - it 'returns http not found' do - get '/api/v1/admin/canonical_email_blocks/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/admin/canonical_email_blocks/test' do - subject do - post '/api/v1/admin/canonical_email_blocks/test', headers: headers, params: params - end - - let(:params) { { email: 'email@example.com' } } - - it_behaves_like 'forbidden for wrong scope', 'read:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - context 'when the required email param is not provided' do - let(:params) { {} } - - it 'returns http bad request' do - subject - - expect(response).to have_http_status(400) - end - end - - context 'when the required email param is provided' do - context 'when there is a matching canonical email block' do - let!(:canonical_email_block) { CanonicalEmailBlock.create(params) } - - it 'returns the expected canonical email hash', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[0][:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash) - end - end - - context 'when there is no matching canonical email block' do - it 'returns an empty list', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to be_empty - end - end - end - end - - describe 'POST /api/v1/admin/canonical_email_blocks' do - subject do - post '/api/v1/admin/canonical_email_blocks', headers: headers, params: params - end - - let(:params) { { email: 'example@email.com' } } - let(:canonical_email_block) { CanonicalEmailBlock.new(email: params[:email]) } - - it_behaves_like 'forbidden for wrong scope', 'read:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'returns the canonical_email_hash correctly', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash) - end - - context 'when the required email param is not provided' do - let(:params) { {} } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the canonical_email_hash param is provided instead of email' do - let(:params) { { canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } } - - it 'returns the correct canonical_email_hash', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:canonical_email_hash]).to eq(params[:canonical_email_hash]) - end - end - - context 'when both email and canonical_email_hash params are provided' do - let(:params) { { email: 'example@email.com', canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } } - - it 'ignores the canonical_email_hash param', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash) - end - end - - context 'when the given canonical email was already blocked' do - before do - canonical_email_block.save - end - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - end - - describe 'DELETE /api/v1/admin/canonical_email_blocks/:id' do - subject do - delete "/api/v1/admin/canonical_email_blocks/#{canonical_email_block.id}", headers: headers - end - - let!(:canonical_email_block) { Fabricate(:canonical_email_block) } - - it_behaves_like 'forbidden for wrong scope', 'read:statuses' - - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'deletes the canonical email block', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(CanonicalEmailBlock.find_by(id: canonical_email_block.id)).to be_nil - end - - context 'when the canonical email block is not found' do - it 'returns http not found' do - delete '/api/v1/admin/canonical_email_blocks/0', headers: headers - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/admin/dimensions_spec.rb b/spec/requests/api/v1/admin/dimensions_spec.rb deleted file mode 100644 index 87534a74b8..0000000000 --- a/spec/requests/api/v1/admin/dimensions_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Admin Dimensions' do - let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account) } - - describe 'GET /api/v1/admin/dimensions' do - context 'when not authorized' do - it 'returns http forbidden' do - post '/api/v1/admin/dimensions', params: { account_id: account.id, limit: 2 } - - expect(response) - .to have_http_status(403) - end - end - - context 'with correct scope' do - let(:scopes) { 'admin:read' } - - it 'returns http success and status json' do - post '/api/v1/admin/dimensions', params: { account_id: account.id, limit: 2 }, headers: headers - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_an(Array) - end - end - end -end diff --git a/spec/requests/api/v1/admin/domain_allows_spec.rb b/spec/requests/api/v1/admin/domain_allows_spec.rb deleted file mode 100644 index b8f0b0055c..0000000000 --- a/spec/requests/api/v1/admin/domain_allows_spec.rb +++ /dev/null @@ -1,174 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Domain Allows' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:read admin:write' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/admin/domain_allows' do - subject do - get '/api/v1/admin/domain_allows', headers: headers, params: params - end - - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - context 'when there is no allowed domains' do - it 'returns an empty body' do - subject - - expect(body_as_json).to be_empty - end - end - - context 'when there are allowed domains' do - let!(:domain_allows) { Fabricate.times(2, :domain_allow) } - let(:expected_response) do - domain_allows.map do |domain_allow| - { - id: domain_allow.id.to_s, - domain: domain_allow.domain, - created_at: domain_allow.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), - } - end - end - - it 'returns the correct allowed domains' do - subject - - expect(body_as_json).to match_array(expected_response) - end - - context 'with limit param' do - let(:params) { { limit: 1 } } - - it 'returns only the requested number of allowed domains' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - end - end - end - - describe 'GET /api/v1/admin/domain_allows/:id' do - subject do - get "/api/v1/admin/domain_allows/#{domain_allow.id}", headers: headers - end - - let!(:domain_allow) { Fabricate(:domain_allow) } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'returns the expected allowed domain name', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:domain]).to eq domain_allow.domain - end - - context 'when the requested allowed domain does not exist' do - it 'returns http not found' do - get '/api/v1/admin/domain_allows/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/admin/domain_allows' do - subject do - post '/api/v1/admin/domain_allows', headers: headers, params: params - end - - let(:params) { { domain: 'foo.bar.com' } } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - context 'with a valid domain name' do - it 'returns the expected domain name', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:domain]).to eq 'foo.bar.com' - expect(DomainAllow.find_by(domain: 'foo.bar.com')).to be_present - end - end - - context 'with invalid domain name' do - let(:params) { { domain: 'foo bar' } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when domain name is not specified' do - let(:params) { {} } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the domain is already allowed' do - before do - DomainAllow.create(params) - end - - it 'returns the existing allowed domain name' do - subject - - expect(body_as_json[:domain]).to eq(params[:domain]) - end - end - end - - describe 'DELETE /api/v1/admin/domain_allows/:id' do - subject do - delete "/api/v1/admin/domain_allows/#{domain_allow.id}", headers: headers - end - - let!(:domain_allow) { Fabricate(:domain_allow) } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'deletes the allowed domain', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(DomainAllow.find_by(id: domain_allow.id)).to be_nil - end - - context 'when the allowed domain does not exist' do - it 'returns http not found' do - delete '/api/v1/admin/domain_allows/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/admin/domain_blocks_spec.rb b/spec/requests/api/v1/admin/domain_blocks_spec.rb deleted file mode 100644 index 415281a932..0000000000 --- a/spec/requests/api/v1/admin/domain_blocks_spec.rb +++ /dev/null @@ -1,275 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Domain Blocks' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:read:domain_blocks admin:write:domain_blocks' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/admin/domain_blocks' do - subject do - get '/api/v1/admin/domain_blocks', headers: headers, params: params - end - - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - context 'when there are no domain blocks' do - it 'returns an empty list' do - subject - - expect(body_as_json).to be_empty - end - end - - context 'when there are domain blocks' do - let!(:domain_blocks) do - [ - Fabricate(:domain_block, severity: :silence, reject_media: true), - Fabricate(:domain_block, severity: :suspend, obfuscate: true), - Fabricate(:domain_block, severity: :noop, reject_reports: true), - Fabricate(:domain_block, public_comment: 'Spam'), - Fabricate(:domain_block, private_comment: 'Spam'), - ] - end - let(:expected_responde) do - domain_blocks.map do |domain_block| - { - id: domain_block.id.to_s, - domain: domain_block.domain, - digest: domain_block.domain_digest, - created_at: domain_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), - severity: domain_block.severity.to_s, - reject_media: domain_block.reject_media, - reject_reports: domain_block.reject_reports, - private_comment: domain_block.private_comment, - public_comment: domain_block.public_comment, - obfuscate: domain_block.obfuscate, - } - end - end - - it 'returns the expected domain blocks' do - subject - - expect(body_as_json).to match_array(expected_responde) - end - - context 'with limit param' do - let(:params) { { limit: 2 } } - - it 'returns only the requested number of domain blocks' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - end - end - end - - describe 'GET /api/v1/admin/domain_blocks/:id' do - subject do - get "/api/v1/admin/domain_blocks/#{domain_block.id}", headers: headers - end - - let!(:domain_block) { Fabricate(:domain_block) } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'returns the expected domain block content', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to eq( - { - id: domain_block.id.to_s, - domain: domain_block.domain, - digest: domain_block.domain_digest, - created_at: domain_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), - severity: domain_block.severity.to_s, - reject_media: domain_block.reject_media, - reject_reports: domain_block.reject_reports, - private_comment: domain_block.private_comment, - public_comment: domain_block.public_comment, - obfuscate: domain_block.obfuscate, - } - ) - end - - context 'when the requested domain block does not exist' do - it 'returns http not found' do - get '/api/v1/admin/domain_blocks/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/admin/domain_blocks' do - subject do - post '/api/v1/admin/domain_blocks', headers: headers, params: params - end - - let(:params) { { domain: 'foo.bar.com', severity: :silence } } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'creates a domain block with the expected domain name and severity', :aggregate_failures do - subject - - body = body_as_json - - expect(response).to have_http_status(200) - expect(body).to match a_hash_including( - { - domain: 'foo.bar.com', - severity: 'silence', - } - ) - - expect(DomainBlock.find_by(domain: 'foo.bar.com')).to be_present - end - - context 'when a looser domain block already exists on a higher level domain' do - let(:params) { { domain: 'foo.bar.com', severity: :suspend } } - - before do - Fabricate(:domain_block, domain: 'bar.com', severity: :silence) - end - - it 'creates a domain block with the expected domain name and severity', :aggregate_failures do - subject - - body = body_as_json - - expect(response).to have_http_status(200) - expect(body).to match a_hash_including( - { - domain: 'foo.bar.com', - severity: 'suspend', - } - ) - - expect(DomainBlock.find_by(domain: 'foo.bar.com')).to be_present - end - end - - context 'when a domain block already exists on the same domain' do - before do - Fabricate(:domain_block, domain: 'foo.bar.com', severity: :silence) - end - - it 'returns existing domain block in error', :aggregate_failures do - subject - - expect(response).to have_http_status(422) - expect(body_as_json[:existing_domain_block][:domain]).to eq('foo.bar.com') - end - end - - context 'when a stricter domain block already exists on a higher level domain' do - before do - Fabricate(:domain_block, domain: 'bar.com', severity: :suspend) - end - - it 'returns existing domain block in error', :aggregate_failures do - subject - - expect(response).to have_http_status(422) - expect(body_as_json[:existing_domain_block][:domain]).to eq('bar.com') - end - end - - context 'when given domain name is invalid' do - let(:params) { { domain: 'foo bar', severity: :silence } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - end - - describe 'PUT /api/v1/admin/domain_blocks/:id' do - subject do - put "/api/v1/admin/domain_blocks/#{domain_block.id}", headers: headers, params: params - end - - let!(:domain_block) { Fabricate(:domain_block, domain: 'example.com', severity: :silence) } - let(:params) { { domain: 'example.com', severity: 'suspend' } } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'returns the updated domain block', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to match a_hash_including( - { - id: domain_block.id.to_s, - domain: domain_block.domain, - digest: domain_block.domain_digest, - severity: 'suspend', - } - ) - end - - it 'updates the block severity' do - expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend') - end - - context 'when domain block does not exist' do - it 'returns http not found' do - put '/api/v1/admin/domain_blocks/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'DELETE /api/v1/admin/domain_blocks/:id' do - subject do - delete "/api/v1/admin/domain_blocks/#{domain_block.id}", headers: headers - end - - let!(:domain_block) { Fabricate(:domain_block) } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'deletes the domain block', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(DomainBlock.find_by(id: domain_block.id)).to be_nil - end - - context 'when domain block does not exist' do - it 'returns http not found' do - delete '/api/v1/admin/domain_blocks/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/admin/email_domain_blocks_spec.rb b/spec/requests/api/v1/admin/email_domain_blocks_spec.rb deleted file mode 100644 index 16656e0202..0000000000 --- a/spec/requests/api/v1/admin/email_domain_blocks_spec.rb +++ /dev/null @@ -1,191 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Email Domain Blocks' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:account) { Fabricate(:account) } - let(:scopes) { 'admin:read:email_domain_blocks admin:write:email_domain_blocks' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/admin/email_domain_blocks' do - subject do - get '/api/v1/admin/email_domain_blocks', headers: headers, params: params - end - - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'read:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - context 'when there is no email domain block' do - it 'returns an empty list' do - subject - - expect(body_as_json).to be_empty - end - end - - context 'when there are email domain blocks' do - let!(:email_domain_blocks) { Fabricate.times(5, :email_domain_block) } - let(:blocked_email_domains) { email_domain_blocks.pluck(:domain) } - - it 'return the correct blocked email domains' do - subject - - expect(body_as_json.pluck(:domain)).to match_array(blocked_email_domains) - end - - context 'with limit param' do - let(:params) { { limit: 2 } } - - it 'returns only the requested number of email domain blocks' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - end - - context 'with since_id param' do - let(:params) { { since_id: email_domain_blocks[1].id } } - - it 'returns only the email domain blocks after since_id' do - subject - - email_domain_blocks_ids = email_domain_blocks.pluck(:id).map(&:to_s) - - expect(body_as_json.pluck(:id)).to match_array(email_domain_blocks_ids[2..]) - end - end - - context 'with max_id param' do - let(:params) { { max_id: email_domain_blocks[3].id } } - - it 'returns only the email domain blocks before max_id' do - subject - - email_domain_blocks_ids = email_domain_blocks.pluck(:id).map(&:to_s) - - expect(body_as_json.pluck(:id)).to match_array(email_domain_blocks_ids[..2]) - end - end - end - end - - describe 'GET /api/v1/admin/email_domain_blocks/:id' do - subject do - get "/api/v1/admin/email_domain_blocks/#{email_domain_block.id}", headers: headers - end - - let!(:email_domain_block) { Fabricate(:email_domain_block) } - - it_behaves_like 'forbidden for wrong scope', 'read:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - context 'when email domain block exists' do - it 'returns the correct blocked domain', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:domain]).to eq(email_domain_block.domain) - end - end - - context 'when email domain block does not exist' do - it 'returns http not found' do - get '/api/v1/admin/email_domain_blocks/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/admin/email_domain_blocks' do - subject do - post '/api/v1/admin/email_domain_blocks', headers: headers, params: params - end - - let(:params) { { domain: 'example.com' } } - - it_behaves_like 'forbidden for wrong scope', 'read:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'returns the correct blocked email domain', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:domain]).to eq(params[:domain]) - end - - context 'when domain param is not provided' do - let(:params) { { domain: '' } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when provided domain name has an invalid character' do - let(:params) { { domain: 'do\uD800.com' } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when provided domain is already blocked' do - before do - EmailDomainBlock.create(params) - end - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - end - - describe 'DELETE /api/v1/admin/email_domain_blocks' do - subject do - delete "/api/v1/admin/email_domain_blocks/#{email_domain_block.id}", headers: headers - end - - let!(:email_domain_block) { Fabricate(:email_domain_block) } - - it_behaves_like 'forbidden for wrong scope', 'read:statuses' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'deletes email domain block', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to be_empty - expect(EmailDomainBlock.find_by(id: email_domain_block.id)).to be_nil - end - - context 'when email domain block does not exist' do - it 'returns http not found' do - delete '/api/v1/admin/email_domain_blocks/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/admin/ip_blocks_spec.rb b/spec/requests/api/v1/admin/ip_blocks_spec.rb deleted file mode 100644 index 98b954dd49..0000000000 --- a/spec/requests/api/v1/admin/ip_blocks_spec.rb +++ /dev/null @@ -1,232 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'IP Blocks' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'admin:read:ip_blocks admin:write:ip_blocks' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/admin/ip_blocks' do - subject do - get '/api/v1/admin/ip_blocks', headers: headers, params: params - end - - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - context 'when there is no ip block' do - it 'returns an empty body' do - subject - - expect(body_as_json).to be_empty - end - end - - context 'when there are ip blocks' do - let!(:ip_blocks) do - [ - IpBlock.create(ip: '192.0.2.0/24', severity: :no_access), - IpBlock.create(ip: '172.16.0.1', severity: :sign_up_requires_approval, comment: 'Spam'), - IpBlock.create(ip: '2001:0db8::/32', severity: :sign_up_block, expires_in: 10.days), - ] - end - let(:expected_response) do - ip_blocks.map do |ip_block| - { - id: ip_block.id.to_s, - ip: ip_block.ip, - severity: ip_block.severity.to_s, - comment: ip_block.comment, - created_at: ip_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), - expires_at: ip_block.expires_at&.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), - } - end - end - - it 'returns the correct blocked ips' do - subject - - expect(body_as_json).to match_array(expected_response) - end - - context 'with limit param' do - let(:params) { { limit: 2 } } - - it 'returns only the requested number of ip blocks' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - end - end - end - - describe 'GET /api/v1/admin/ip_blocks/:id' do - subject do - get "/api/v1/admin/ip_blocks/#{ip_block.id}", headers: headers - end - - let!(:ip_block) { IpBlock.create(ip: '192.0.2.0/24', severity: :no_access) } - - it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'returns the correct ip block', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - json = body_as_json - - expect(json[:ip]).to eq("#{ip_block.ip}/#{ip_block.ip.prefix}") - expect(json[:severity]).to eq(ip_block.severity.to_s) - end - - context 'when ip block does not exist' do - it 'returns http not found' do - get '/api/v1/admin/ip_blocks/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/admin/ip_blocks' do - subject do - post '/api/v1/admin/ip_blocks', headers: headers, params: params - end - - let(:params) { { ip: '151.0.32.55', severity: 'no_access', comment: 'Spam' } } - - it_behaves_like 'forbidden for wrong scope', 'admin:read:ip_blocks' - it_behaves_like 'forbidden for wrong role', '' - it_behaves_like 'forbidden for wrong role', 'Moderator' - - it 'returns the correct ip block', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - json = body_as_json - - expect(json[:ip]).to eq("#{params[:ip]}/32") - expect(json[:severity]).to eq(params[:severity]) - expect(json[:comment]).to eq(params[:comment]) - end - - context 'when the required ip param is not provided' do - let(:params) { { ip: '', severity: 'no_access' } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the required severity param is not provided' do - let(:params) { { ip: '173.65.23.1', severity: '' } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the given ip address is already blocked' do - before do - IpBlock.create(params) - end - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the given ip address is invalid' do - let(:params) { { ip: '520.13.54.120', severity: 'no_access' } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - end - - describe 'PUT /api/v1/admin/ip_blocks/:id' do - subject do - put "/api/v1/admin/ip_blocks/#{ip_block.id}", headers: headers, params: params - end - - let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access', comment: 'Spam', expires_in: 48.hours) } - let(:params) { { severity: 'sign_up_requires_approval', comment: 'Decreasing severity' } } - - it 'returns the correct ip block', :aggregate_failures do - expect { subject } - .to change_severity_level - .and change_comment_value - - expect(response).to have_http_status(200) - expect(body_as_json).to match(hash_including({ - ip: "#{ip_block.ip}/#{ip_block.ip.prefix}", - severity: 'sign_up_requires_approval', - comment: 'Decreasing severity', - })) - end - - def change_severity_level - change { ip_block.reload.severity }.from('no_access').to('sign_up_requires_approval') - end - - def change_comment_value - change { ip_block.reload.comment }.from('Spam').to('Decreasing severity') - end - - context 'when ip block does not exist' do - it 'returns http not found' do - put '/api/v1/admin/ip_blocks/-1', headers: headers, params: params - - expect(response).to have_http_status(404) - end - end - end - - describe 'DELETE /api/v1/admin/ip_blocks/:id' do - subject do - delete "/api/v1/admin/ip_blocks/#{ip_block.id}", headers: headers - end - - let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access') } - - it 'deletes the ip block', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to be_empty - expect(IpBlock.find_by(id: ip_block.id)).to be_nil - end - - context 'when ip block does not exist' do - it 'returns http not found' do - delete '/api/v1/admin/ip_blocks/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/admin/measures_spec.rb b/spec/requests/api/v1/admin/measures_spec.rb deleted file mode 100644 index 80fed79d9a..0000000000 --- a/spec/requests/api/v1/admin/measures_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Admin Measures' do - let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account) } - let(:params) do - { - keys: %w(instance_accounts instance_follows instance_followers), - instance_accounts: { - domain: 'mastodon.social', - include_subdomains: true, - }, - instance_follows: { - domain: 'mastodon.social', - include_subdomains: true, - }, - instance_followers: { - domain: 'mastodon.social', - include_subdomains: true, - }, - } - end - - describe 'GET /api/v1/admin/measures' do - context 'when not authorized' do - it 'returns http forbidden' do - post '/api/v1/admin/measures', params: params - - expect(response) - .to have_http_status(403) - end - end - - context 'with correct scope' do - let(:scopes) { 'admin:read' } - - it 'returns http success and status json' do - post '/api/v1/admin/measures', params: params, headers: headers - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_an(Array) - end - end - end -end diff --git a/spec/requests/api/v1/admin/reports_spec.rb b/spec/requests/api/v1/admin/reports_spec.rb deleted file mode 100644 index 4b0b7e1713..0000000000 --- a/spec/requests/api/v1/admin/reports_spec.rb +++ /dev/null @@ -1,255 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Reports' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:read:reports admin:write:reports' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/admin/reports' do - subject do - get '/api/v1/admin/reports', headers: headers, params: params - end - - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - context 'when there are no reports' do - it 'returns an empty list' do - subject - - expect(body_as_json).to be_empty - end - end - - context 'when there are reports' do - let!(:reporter) { Fabricate(:account) } - let!(:spammer) { Fabricate(:account) } - let(:expected_response) do - scope.map do |report| - hash_including({ - id: report.id.to_s, - action_taken: report.action_taken?, - category: report.category, - comment: report.comment, - account: hash_including(id: report.account.id.to_s), - target_account: hash_including(id: report.target_account.id.to_s), - statuses: report.statuses, - rules: report.rules, - forwarded: report.forwarded, - }) - end - end - let(:scope) { Report.unresolved } - - before do - Fabricate(:report) - Fabricate(:report, target_account: spammer) - Fabricate(:report, account: reporter, target_account: spammer) - Fabricate(:report, action_taken_at: 4.days.ago, account: reporter) - Fabricate(:report, action_taken_at: 20.days.ago) - end - - it 'returns all unresolved reports' do - subject - - expect(body_as_json).to match_array(expected_response) - end - - context 'with resolved param' do - let(:params) { { resolved: true } } - let(:scope) { Report.resolved } - - it 'returns only the resolved reports' do - subject - - expect(body_as_json).to match_array(expected_response) - end - end - - context 'with account_id param' do - let(:params) { { account_id: reporter.id } } - let(:scope) { Report.unresolved.where(account: reporter) } - - it 'returns all unresolved reports filed by the specified account' do - subject - - expect(body_as_json).to match_array(expected_response) - end - end - - context 'with target_account_id param' do - let(:params) { { target_account_id: spammer.id } } - let(:scope) { Report.unresolved.where(target_account: spammer) } - - it 'returns all unresolved reports targeting the specified account' do - subject - - expect(body_as_json).to match_array(expected_response) - end - end - - context 'with limit param' do - let(:params) { { limit: 1 } } - - it 'returns only the requested number of reports' do - subject - - expect(body_as_json.size).to eq(1) - end - end - end - end - - describe 'GET /api/v1/admin/reports/:id' do - subject do - get "/api/v1/admin/reports/#{report.id}", headers: headers - end - - let(:report) { Fabricate(:report) } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns the requested report content', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to include( - { - id: report.id.to_s, - action_taken: report.action_taken?, - category: report.category, - comment: report.comment, - account: a_hash_including(id: report.account.id.to_s), - target_account: a_hash_including(id: report.target_account.id.to_s), - statuses: report.statuses, - rules: report.rules, - forwarded: report.forwarded, - } - ) - end - end - - describe 'PUT /api/v1/admin/reports/:id' do - subject do - put "/api/v1/admin/reports/#{report.id}", headers: headers, params: params - end - - let!(:report) { Fabricate(:report, category: :other) } - let(:params) { { category: 'spam' } } - - it 'updates the report category', :aggregate_failures do - expect { subject } - .to change { report.reload.category }.from('other').to('spam') - .and create_an_action_log - - expect(response).to have_http_status(200) - - report.reload - - expect(body_as_json).to include( - { - id: report.id.to_s, - action_taken: report.action_taken?, - category: report.category, - comment: report.comment, - account: a_hash_including(id: report.account.id.to_s), - target_account: a_hash_including(id: report.target_account.id.to_s), - statuses: report.statuses, - rules: report.rules, - forwarded: report.forwarded, - } - ) - end - end - - describe 'POST #resolve' do - subject do - post "/api/v1/admin/reports/#{report.id}/resolve", headers: headers - end - - let(:report) { Fabricate(:report, action_taken_at: nil) } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'marks report as resolved', :aggregate_failures do - expect { subject } - .to change { report.reload.unresolved? }.from(true).to(false) - .and create_an_action_log - expect(response).to have_http_status(200) - end - end - - describe 'POST #reopen' do - subject do - post "/api/v1/admin/reports/#{report.id}/reopen", headers: headers - end - - let(:report) { Fabricate(:report, action_taken_at: 10.days.ago) } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'marks report as unresolved', :aggregate_failures do - expect { subject } - .to change { report.reload.unresolved? }.from(false).to(true) - .and create_an_action_log - expect(response).to have_http_status(200) - end - end - - describe 'POST #assign_to_self' do - subject do - post "/api/v1/admin/reports/#{report.id}/assign_to_self", headers: headers - end - - let(:report) { Fabricate(:report) } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'assigns report to the requesting user', :aggregate_failures do - expect { subject } - .to change { report.reload.assigned_account_id }.from(nil).to(user.account.id) - .and create_an_action_log - expect(response).to have_http_status(200) - end - end - - describe 'POST #unassign' do - subject do - post "/api/v1/admin/reports/#{report.id}/unassign", headers: headers - end - - let(:report) { Fabricate(:report, assigned_account_id: user.account.id) } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'unassigns report from assignee', :aggregate_failures do - expect { subject } - .to change { report.reload.assigned_account_id }.from(user.account.id).to(nil) - .and create_an_action_log - expect(response).to have_http_status(200) - end - end - - private - - def create_an_action_log - change(Admin::ActionLog, :count).by(1) - end -end diff --git a/spec/requests/api/v1/admin/retention_spec.rb b/spec/requests/api/v1/admin/retention_spec.rb deleted file mode 100644 index 9178335ba5..0000000000 --- a/spec/requests/api/v1/admin/retention_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Admin Retention' do - let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account) } - - describe 'GET /api/v1/admin/retention' do - context 'when not authorized' do - it 'returns http forbidden' do - post '/api/v1/admin/retention', params: { account_id: account.id, limit: 2 } - - expect(response) - .to have_http_status(403) - end - end - - context 'with correct scope' do - let(:scopes) { 'admin:read' } - - it 'returns http success and status json' do - post '/api/v1/admin/retention', params: { account_id: account.id, limit: 2 }, headers: headers - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_an(Array) - end - end - end -end diff --git a/spec/requests/api/v1/admin/tags_spec.rb b/spec/requests/api/v1/admin/tags_spec.rb deleted file mode 100644 index 031be17f52..0000000000 --- a/spec/requests/api/v1/admin/tags_spec.rb +++ /dev/null @@ -1,141 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Tags' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:read admin:write' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:tag) { Fabricate(:tag) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/admin/tags' do - subject do - get '/api/v1/admin/tags', headers: headers, params: params - end - - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - context 'when there are no tags' do - it 'returns an empty list' do - subject - - expect(body_as_json).to be_empty - end - end - - context 'when there are tagss' do - let!(:tags) do - [ - Fabricate(:tag), - Fabricate(:tag), - Fabricate(:tag), - Fabricate(:tag), - ] - end - - it 'returns the expected tags' do - subject - tags.each do |tag| - expect(body_as_json.find { |item| item[:id] == tag.id.to_s && item[:name] == tag.name }).to_not be_nil - end - end - - context 'with limit param' do - let(:params) { { limit: 2 } } - - it 'returns only the requested number of tags' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - end - end - end - - describe 'GET /api/v1/admin/tags/:id' do - subject do - get "/api/v1/admin/tags/#{tag.id}", headers: headers - end - - let!(:tag) { Fabricate(:tag) } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'returns expected tag content' do - subject - - expect(body_as_json[:id].to_i).to eq(tag.id) - expect(body_as_json[:name]).to eq(tag.name) - end - - context 'when the requested tag does not exist' do - it 'returns http not found' do - get '/api/v1/admin/tags/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'PUT /api/v1/admin/tags/:id' do - subject do - put "/api/v1/admin/tags/#{tag.id}", headers: headers, params: params - end - - let!(:tag) { Fabricate(:tag) } - let(:params) { { display_name: tag.name.upcase } } - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong scope', 'admin:read' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'returns updated tag' do - subject - - expect(body_as_json[:id].to_i).to eq(tag.id) - expect(body_as_json[:name]).to eq(tag.name.upcase) - end - - context 'when the updated display name is invalid' do - let(:params) { { display_name: tag.name + tag.id.to_s } } - - it 'returns http unprocessable content' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the requested tag does not exist' do - it 'returns http not found' do - get '/api/v1/admin/tags/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/admin/trends/links/links_spec.rb b/spec/requests/api/v1/admin/trends/links/links_spec.rb deleted file mode 100644 index 48842828b3..0000000000 --- a/spec/requests/api/v1/admin/trends/links/links_spec.rb +++ /dev/null @@ -1,130 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Links' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:read admin:write' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/admin/trends/links' do - subject do - get '/api/v1/admin/trends/links', headers: headers - end - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - end - - describe 'POST /api/v1/admin/trends/links/:id/approve' do - subject do - post "/api/v1/admin/trends/links/#{preview_card.id}/approve", headers: headers - end - - let(:preview_card) { Fabricate(:preview_card, trendable: false) } - - it_behaves_like 'forbidden for wrong scope', 'read write' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - expect { subject } - .to change_link_trendable_to_true - - expect(response).to have_http_status(200) - expects_correct_link_data - end - - def change_link_trendable_to_true - change { preview_card.reload.trendable }.from(false).to(true) - end - - def expects_correct_link_data - expect(body_as_json).to match( - a_hash_including( - url: preview_card.url, - title: preview_card.title, - description: preview_card.description, - type: 'link', - requires_review: false - ) - ) - end - - context 'when the link does not exist' do - it 'returns http not found' do - post '/api/v1/admin/trends/links/-1/approve', headers: headers - - expect(response).to have_http_status(404) - end - end - - context 'without an authorization header' do - let(:headers) { {} } - - it 'returns http forbidden' do - subject - - expect(response).to have_http_status(403) - end - end - end - - describe 'POST /api/v1/admin/trends/links/:id/reject' do - subject do - post "/api/v1/admin/trends/links/#{preview_card.id}/reject", headers: headers - end - - let(:preview_card) { Fabricate(:preview_card, trendable: false) } - - it_behaves_like 'forbidden for wrong scope', 'read write' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - expect { subject } - .to_not change_link_trendable - - expect(response).to have_http_status(200) - end - - def change_link_trendable - change { preview_card.reload.trendable } - end - - it 'returns the link data' do - subject - - expect(body_as_json).to match( - a_hash_including( - url: preview_card.url, - title: preview_card.title, - description: preview_card.description, - type: 'link', - requires_review: false - ) - ) - end - - context 'when the link does not exist' do - it 'returns http not found' do - post '/api/v1/admin/trends/links/-1/reject', headers: headers - - expect(response).to have_http_status(404) - end - end - - context 'without an authorization header' do - let(:headers) { {} } - - it 'returns http forbidden' do - subject - - expect(response).to have_http_status(403) - end - end - end -end diff --git a/spec/requests/api/v1/admin/trends/links/preview_card_providers_spec.rb b/spec/requests/api/v1/admin/trends/links/preview_card_providers_spec.rb deleted file mode 100644 index 384a305d4a..0000000000 --- a/spec/requests/api/v1/admin/trends/links/preview_card_providers_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Admin Trends Links Preview Card Providers' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:read admin:write' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account) } - let(:preview_card_provider) { Fabricate(:preview_card_provider) } - - describe 'GET /api/v1/admin/trends/links/publishers' do - it 'returns http success' do - get '/api/v1/admin/trends/links/publishers', params: { account_id: account.id, limit: 2 }, headers: headers - - expect(response).to have_http_status(200) - end - end - - describe 'POST /api/v1/admin/trends/links/publishers/:id/approve' do - before do - post "/api/v1/admin/trends/links/publishers/#{preview_card_provider.id}/approve", headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - expect(response).to have_http_status(200) - end - end - - describe 'POST /api/v1/admin/trends/links/publishers/:id/reject' do - before do - post "/api/v1/admin/trends/links/publishers/#{preview_card_provider.id}/reject", headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/v1/admin/trends/statuses_spec.rb b/spec/requests/api/v1/admin/trends/statuses_spec.rb deleted file mode 100644 index 04aa0465f2..0000000000 --- a/spec/requests/api/v1/admin/trends/statuses_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Admin Trends Statuses' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:read admin:write' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:account) { Fabricate(:account) } - let(:status) { Fabricate(:status) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/admin/trends/statuses' do - it 'returns http success' do - get '/api/v1/admin/trends/statuses', params: { account_id: account.id, limit: 2 }, headers: headers - - expect(response).to have_http_status(200) - end - end - - describe 'POST /api/v1/admin/trends/statuses/:id/approve' do - before do - post "/api/v1/admin/trends/statuses/#{status.id}/approve", headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - expect(response).to have_http_status(200) - end - end - - describe 'POST /api/v1/admin/trends/statuses/:id/unapprove' do - before do - post "/api/v1/admin/trends/statuses/#{status.id}/reject", headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/v1/admin/trends/tags_spec.rb b/spec/requests/api/v1/admin/trends/tags_spec.rb deleted file mode 100644 index b1437dad8d..0000000000 --- a/spec/requests/api/v1/admin/trends/tags_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Admin Trends Tags' do - let(:role) { UserRole.find_by(name: 'Admin') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:read admin:write' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:account) { Fabricate(:account) } - let(:tag) { Fabricate(:tag) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/admin/trends/tags' do - it 'returns http success' do - get '/api/v1/admin/trends/tags', params: { account_id: account.id, limit: 2 }, headers: headers - - expect(response).to have_http_status(200) - end - end - - describe 'POST /api/v1/admin/trends/tags/:id/approve' do - before do - post "/api/v1/admin/trends/tags/#{tag.id}/approve", headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - expect(response).to have_http_status(200) - end - end - - describe 'POST /api/v1/admin/trends/tags/:id/reject' do - before do - post "/api/v1/admin/trends/tags/#{tag.id}/reject", headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - it 'returns http success' do - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/v1/announcements/reactions_spec.rb b/spec/requests/api/v1/announcements/reactions_spec.rb deleted file mode 100644 index ffacb2b0af..0000000000 --- a/spec/requests/api/v1/announcements/reactions_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V1 Announcements Reactions' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'write:favourites' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - let!(:announcement) { Fabricate(:announcement) } - - describe 'PUT /api/v1/announcements/:announcement_id/reactions/:id' do - context 'without token' do - it 'returns http unauthorized' do - put "/api/v1/announcements/#{announcement.id}/reactions/#{escaped_emoji}" - - expect(response).to have_http_status 401 - end - end - - context 'with token' do - before do - put "/api/v1/announcements/#{announcement.id}/reactions/#{escaped_emoji}", headers: headers - end - - it 'creates reaction', :aggregate_failures do - expect(response).to have_http_status(200) - expect(announcement.announcement_reactions.find_by(name: '😂', account: user.account)).to_not be_nil - end - end - end - - describe 'DELETE /api/v1/announcements/:announcement_id/reactions/:id' do - before do - announcement.announcement_reactions.create!(account: user.account, name: '😂') - end - - context 'without token' do - it 'returns http unauthorized' do - delete "/api/v1/announcements/#{announcement.id}/reactions/#{escaped_emoji}" - expect(response).to have_http_status 401 - end - end - - context 'with token' do - before do - delete "/api/v1/announcements/#{announcement.id}/reactions/#{escaped_emoji}", headers: headers - end - - it 'creates reaction', :aggregate_failures do - expect(response).to have_http_status(200) - expect(announcement.announcement_reactions.find_by(name: '😂', account: user.account)).to be_nil - end - end - end - - def escaped_emoji - CGI.escape('😂') - end -end diff --git a/spec/requests/api/v1/announcements_spec.rb b/spec/requests/api/v1/announcements_spec.rb deleted file mode 100644 index 1624b76012..0000000000 --- a/spec/requests/api/v1/announcements_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V1 Announcements' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - let!(:announcement) { Fabricate(:announcement) } - - describe 'GET /api/v1/announcements' do - context 'without token' do - it 'returns http unprocessable entity' do - get '/api/v1/announcements' - - expect(response).to have_http_status 422 - end - end - - context 'with token' do - before do - get '/api/v1/announcements', headers: headers - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - end - end - - describe 'POST /api/v1/announcements/:id/dismiss' do - context 'without token' do - it 'returns http unauthorized' do - post "/api/v1/announcements/#{announcement.id}/dismiss" - - expect(response).to have_http_status 401 - end - end - - context 'with token' do - let(:scopes) { 'write:accounts' } - - before do - post "/api/v1/announcements/#{announcement.id}/dismiss", headers: headers - end - - it 'dismisses announcement', :aggregate_failures do - expect(response).to have_http_status(200) - expect(announcement.announcement_mutes.find_by(account: user.account)).to_not be_nil - end - end - end -end diff --git a/spec/requests/api/v1/apps/credentials_spec.rb b/spec/requests/api/v1/apps/credentials_spec.rb deleted file mode 100644 index 6e6970ce53..0000000000 --- a/spec/requests/api/v1/apps/credentials_spec.rb +++ /dev/null @@ -1,128 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Credentials' do - describe 'GET /api/v1/apps/verify_credentials' do - subject do - get '/api/v1/apps/verify_credentials', headers: headers - end - - context 'with an oauth token' do - let(:application) { Fabricate(:application, scopes: 'read') } - let(:token) { Fabricate(:accessible_access_token, application: application) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - it 'returns the app information correctly', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - - expect(body_as_json).to match( - a_hash_including( - id: token.application.id.to_s, - name: token.application.name, - website: token.application.website, - scopes: token.application.scopes.map(&:to_s), - redirect_uris: token.application.redirect_uris, - # Deprecated properties as of 4.3: - redirect_uri: token.application.redirect_uri.split.first, - vapid_key: Rails.configuration.x.vapid_public_key - ) - ) - end - - it 'does not expose the client_id or client_secret' do - subject - - expect(response).to have_http_status(200) - - expect(body_as_json[:client_id]).to_not be_present - expect(body_as_json[:client_secret]).to_not be_present - end - end - - context 'with a non-read scoped oauth token' do - let(:application) { Fabricate(:application, scopes: 'admin:write') } - let(:token) { Fabricate(:accessible_access_token, application: application) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'returns the app information correctly' do - subject - - expect(body_as_json).to match( - a_hash_including( - id: token.application.id.to_s, - name: token.application.name, - website: token.application.website, - scopes: token.application.scopes.map(&:to_s), - redirect_uris: token.application.redirect_uris, - # Deprecated properties as of 4.3: - redirect_uri: token.application.redirect_uri.split.first, - vapid_key: Rails.configuration.x.vapid_public_key - ) - ) - end - end - - context 'without an oauth token' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - - context 'with a revoked oauth token' do - let(:application) { Fabricate(:application, scopes: 'read') } - let(:token) { Fabricate(:accessible_access_token, application: application, revoked_at: DateTime.now.utc) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - it 'returns http authorization error' do - subject - - expect(response).to have_http_status(401) - end - - it 'returns the error in the json response' do - subject - - expect(body_as_json).to match( - a_hash_including( - error: 'The access token was revoked' - ) - ) - end - end - - context 'with an invalid oauth token' do - let(:application) { Fabricate(:application, scopes: 'read') } - let(:token) { Fabricate(:accessible_access_token, application: application) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}-invalid" } } - - it 'returns http authorization error' do - subject - - expect(response).to have_http_status(401) - end - - it 'returns the error in the json response' do - subject - - expect(body_as_json).to match( - a_hash_including( - error: 'The access token is invalid' - ) - ) - end - end - end -end diff --git a/spec/requests/api/v1/apps_spec.rb b/spec/requests/api/v1/apps_spec.rb deleted file mode 100644 index 1f01bddf3c..0000000000 --- a/spec/requests/api/v1/apps_spec.rb +++ /dev/null @@ -1,249 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Apps' do - describe 'POST /api/v1/apps' do - subject do - post '/api/v1/apps', params: params - end - - let(:client_name) { 'Test app' } - let(:scopes) { 'read write' } - let(:redirect_uri) { 'urn:ietf:wg:oauth:2.0:oob' } - let(:redirect_uris) { [redirect_uri] } - let(:website) { nil } - - let(:params) do - { - client_name: client_name, - redirect_uris: redirect_uris, - scopes: scopes, - website: website, - } - end - - context 'with valid params' do - it 'creates an OAuth app', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - - app = Doorkeeper::Application.find_by(name: client_name) - - expect(app).to be_present - expect(app.scopes.to_s).to eq scopes - expect(app.redirect_uris).to eq redirect_uris - - expect(body_as_json).to match( - a_hash_including( - id: app.id.to_s, - client_id: app.uid, - client_secret: app.secret, - name: client_name, - website: website, - scopes: ['read', 'write'], - redirect_uris: redirect_uris, - # Deprecated properties as of 4.3: - redirect_uri: redirect_uri, - vapid_key: Rails.configuration.x.vapid_public_key - ) - ) - end - end - - context 'without scopes being supplied' do - let(:scopes) { nil } - - it 'creates an OAuth App with the default scope' do - subject - - expect(response).to have_http_status(200) - expect(Doorkeeper::Application.find_by(name: client_name)).to be_present - - body = body_as_json - - expect(body[:scopes]).to eq Doorkeeper.config.default_scopes.to_a - end - end - - # FIXME: This is a bug: https://github.com/mastodon/mastodon/issues/30152 - context 'with scopes as an array' do - let(:scopes) { %w(read write follow) } - - it 'creates an OAuth App with the default scope' do - subject - - expect(response).to have_http_status(200) - - app = Doorkeeper::Application.find_by(name: client_name) - - expect(app).to be_present - expect(app.scopes.to_s).to eq 'read' - - body = body_as_json - - expect(body[:scopes]).to eq ['read'] - end - end - - context 'with an unsupported scope' do - let(:scopes) { 'hoge' } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'with many duplicate scopes' do - let(:scopes) { (%w(read) * 40).join(' ') } - - it 'only saves the scope once', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(Doorkeeper::Application.find_by(name: client_name).scopes.to_s).to eq 'read' - end - end - - context 'with a too-long name' do - let(:client_name) { 'hoge' * 20 } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'with a too-long website' do - let(:website) { "https://foo.bar/#{'hoge' * 2_000}" } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'with a too-long redirect_uri' do - let(:redirect_uris) { "https://app.example/#{'hoge' * 2_000}" } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - # NOTE: This spec currently tests the same as the "with a too-long redirect_uri test case" - context 'with too many redirect_uris' do - let(:redirect_uris) { (0...500).map { |i| "https://app.example/#{i}/callback" } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'with multiple redirect_uris as a string' do - let(:redirect_uris) { "https://redirect1.example/\napp://redirect2.example/" } - - it 'creates an OAuth application with multiple redirect URIs' do - subject - - expect(response).to have_http_status(200) - - app = Doorkeeper::Application.find_by(name: client_name) - - expect(app).to be_present - expect(app.redirect_uri).to eq redirect_uris - expect(app.redirect_uris).to eq redirect_uris.split - - body = body_as_json - - expect(body[:redirect_uri]).to eq redirect_uris - expect(body[:redirect_uris]).to eq redirect_uris.split - end - end - - context 'with multiple redirect_uris as an array' do - let(:redirect_uris) { ['https://redirect1.example/', 'app://redirect2.example/'] } - - it 'creates an OAuth application with multiple redirect URIs' do - subject - - expect(response).to have_http_status(200) - - app = Doorkeeper::Application.find_by(name: client_name) - - expect(app).to be_present - expect(app.redirect_uri).to eq redirect_uris.join "\n" - expect(app.redirect_uris).to eq redirect_uris - - body = body_as_json - - expect(body[:redirect_uri]).to eq redirect_uris.join "\n" - expect(body[:redirect_uris]).to eq redirect_uris - end - end - - context 'with an empty redirect_uris array' do - let(:redirect_uris) { [] } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'with just a newline as the redirect_uris string' do - let(:redirect_uris) { "\n" } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'with an empty redirect_uris string' do - let(:redirect_uris) { '' } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'without a required param' do - let(:client_name) { '' } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'with a website' do - let(:website) { 'https://app.example/' } - - it 'creates an OAuth application with the website specified' do - subject - - expect(response).to have_http_status(200) - - app = Doorkeeper::Application.find_by(name: client_name) - - expect(app).to be_present - expect(app.website).to eq website - end - end - end -end diff --git a/spec/requests/api/v1/blocks_spec.rb b/spec/requests/api/v1/blocks_spec.rb deleted file mode 100644 index c6c2d56f36..0000000000 --- a/spec/requests/api/v1/blocks_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Blocks' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:blocks' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/blocks' do - subject do - get '/api/v1/blocks', headers: headers, params: params - end - - let!(:blocks) { Fabricate.times(3, :block, account: user.account) } - let(:params) { {} } - - let(:expected_response) do - blocks.map { |block| a_hash_including(id: block.target_account.id.to_s, username: block.target_account.username) } - end - - it_behaves_like 'forbidden for wrong scope', 'write write:blocks' - - it 'returns the blocked accounts', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to match_array(expected_response) - end - - context 'with limit param' do - let(:params) { { limit: 2 } } - - it 'returns only the requested number of blocked accounts' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - - it 'sets correct link header pagination' do - subject - - expect(response) - .to include_pagination_headers( - prev: api_v1_blocks_url(limit: params[:limit], since_id: blocks.last.id), - next: api_v1_blocks_url(limit: params[:limit], max_id: blocks.second.id) - ) - end - end - - context 'with max_id param' do - let(:params) { { max_id: blocks[1].id } } - - it 'queries the blocks in range according to max_id', :aggregate_failures do - subject - - response_body = body_as_json - - expect(response_body.size).to be 1 - expect(response_body[0][:id]).to eq(blocks[0].target_account.id.to_s) - end - end - - context 'with since_id param' do - let(:params) { { since_id: blocks[1].id } } - - it 'queries the blocks in range according to since_id', :aggregate_failures do - subject - - response_body = body_as_json - - expect(response_body.size).to be 1 - expect(response_body[0][:id]).to eq(blocks[2].target_account.id.to_s) - end - end - end -end diff --git a/spec/requests/api/v1/bookmarks_spec.rb b/spec/requests/api/v1/bookmarks_spec.rb deleted file mode 100644 index dc32820c89..0000000000 --- a/spec/requests/api/v1/bookmarks_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Bookmarks' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:bookmarks' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/bookmarks' do - subject do - get '/api/v1/bookmarks', headers: headers, params: params - end - - let(:params) { {} } - let!(:bookmarks) { Fabricate.times(2, :bookmark, account: user.account) } - - let(:expected_response) do - bookmarks.map do |bookmark| - a_hash_including(id: bookmark.status.id.to_s, account: a_hash_including(id: bookmark.status.account.id.to_s)) - end - end - - it_behaves_like 'forbidden for wrong scope', 'write' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'returns the bookmarked statuses' do - subject - - expect(body_as_json).to match_array(expected_response) - end - - context 'with limit param' do - let(:params) { { limit: 1 } } - - it 'paginates correctly', :aggregate_failures do - subject - - expect(body_as_json.size) - .to eq(params[:limit]) - - expect(response) - .to include_pagination_headers( - prev: api_v1_bookmarks_url(limit: params[:limit], min_id: bookmarks.last.id), - next: api_v1_bookmarks_url(limit: params[:limit], max_id: bookmarks.second.id) - ) - end - end - - context 'without the authorization header' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v1/conversations_spec.rb b/spec/requests/api/v1/conversations_spec.rb deleted file mode 100644 index f136e1f4e8..0000000000 --- a/spec/requests/api/v1/conversations_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V1 Conversations' do - let!(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) } - let(:scopes) { 'read:statuses' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - let(:other) { Fabricate(:user) } - - describe 'GET /api/v1/conversations', :inline_jobs do - before do - user.account.follow!(other.account) - PostStatusService.new.call(other.account, text: 'Hey @alice', visibility: 'direct') - PostStatusService.new.call(user.account, text: 'Hey, nobody here', visibility: 'direct') - end - - it 'returns pagination headers', :aggregate_failures do - get '/api/v1/conversations', params: { limit: 1 }, headers: headers - - expect(response) - .to have_http_status(200) - .and include_pagination_headers( - prev: api_v1_conversations_url(limit: 1, min_id: Status.first.id), - next: api_v1_conversations_url(limit: 1, max_id: Status.first.id) - ) - end - - it 'returns conversations', :aggregate_failures do - get '/api/v1/conversations', headers: headers - - expect(body_as_json.size).to eq 2 - expect(body_as_json[0][:accounts].size).to eq 1 - end - - context 'with since_id' do - context 'when requesting old posts' do - it 'returns conversations' do - get '/api/v1/conversations', params: { since_id: Mastodon::Snowflake.id_at(1.hour.ago, with_random: false) }, headers: headers - - expect(body_as_json.size).to eq 2 - end - end - - context 'when requesting posts in the future' do - it 'returns no conversation' do - get '/api/v1/conversations', params: { since_id: Mastodon::Snowflake.id_at(1.hour.from_now, with_random: false) }, headers: headers - - expect(body_as_json.size).to eq 0 - end - end - end - end -end diff --git a/spec/requests/api/v1/csp_spec.rb b/spec/requests/api/v1/csp_spec.rb deleted file mode 100644 index 2db52ac725..0000000000 --- a/spec/requests/api/v1/csp_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API namespace minimal Content-Security-Policy' do - before { stub_tests_controller } - - after { Rails.application.reload_routes! } - - it 'returns the correct CSP headers' do - get '/api/v1/tests' - - expect(response).to have_http_status(200) - expect(response.headers['Content-Security-Policy']).to eq(minimal_csp_headers) - end - - private - - def stub_tests_controller - stub_const('Api::V1::TestsController', api_tests_controller) - - Rails.application.routes.draw do - get '/api/v1/tests', to: 'api/v1/tests#index' - end - end - - def api_tests_controller - Class.new(Api::BaseController) do - def index - head 200 - end - - private - - def user_signed_in? = false - def current_user = nil - end - end - - def minimal_csp_headers - "default-src 'none'; frame-ancestors 'none'; form-action 'none'" - end -end diff --git a/spec/requests/api/v1/custom_emojis_spec.rb b/spec/requests/api/v1/custom_emojis_spec.rb deleted file mode 100644 index 2f0dc72944..0000000000 --- a/spec/requests/api/v1/custom_emojis_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Custom Emojis' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/custom_emojis' do - before do - Fabricate(:custom_emoji, domain: nil, disabled: false, visible_in_picker: true, shortcode: 'coolcat') - end - - context 'when logged out' do - it 'returns http success and json' do - get api_v1_custom_emojis_path - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - .and have_attributes( - first: include(shortcode: 'coolcat') - ) - end - end - - context 'when logged in' do - it 'returns http success and json' do - get api_v1_custom_emojis_path, headers: headers - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - .and have_attributes( - first: include(shortcode: 'coolcat') - ) - end - end - end -end diff --git a/spec/requests/api/v1/directories_spec.rb b/spec/requests/api/v1/directories_spec.rb deleted file mode 100644 index 0a1864d136..0000000000 --- a/spec/requests/api/v1/directories_spec.rb +++ /dev/null @@ -1,139 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Directories API' do - let(:user) { Fabricate(:user, confirmed_at: nil) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:follows' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/directories' do - context 'with no params' do - before do - local_unconfirmed_account = Fabricate( - :account, - domain: nil, - user: Fabricate(:user, confirmed_at: nil, approved: true), - username: 'local_unconfirmed' - ) - local_unconfirmed_account.create_account_stat! - - local_unapproved_account = Fabricate( - :account, - domain: nil, - user: Fabricate(:user, confirmed_at: 10.days.ago), - username: 'local_unapproved' - ) - local_unapproved_account.create_account_stat! - local_unapproved_account.user.update(approved: false) - - local_undiscoverable_account = Fabricate( - :account, - domain: nil, - user: Fabricate(:user, confirmed_at: 10.days.ago, approved: true), - discoverable: false, - username: 'local_undiscoverable' - ) - local_undiscoverable_account.create_account_stat! - - excluded_from_timeline_account = Fabricate( - :account, - domain: 'host.example', - discoverable: true, - username: 'remote_excluded_from_timeline' - ) - excluded_from_timeline_account.create_account_stat! - Fabricate(:block, account: user.account, target_account: excluded_from_timeline_account) - - domain_blocked_account = Fabricate( - :account, - domain: 'test.example', - discoverable: true, - username: 'remote_domain_blocked' - ) - domain_blocked_account.create_account_stat! - Fabricate(:account_domain_block, account: user.account, domain: 'test.example') - - local_discoverable_account.create_account_stat! - eligible_remote_account.create_account_stat! - end - - let(:local_discoverable_account) do - Fabricate( - :account, - domain: nil, - user: Fabricate(:user, confirmed_at: 10.days.ago, approved: true), - discoverable: true, - username: 'local_discoverable' - ) - end - - let(:eligible_remote_account) do - Fabricate( - :account, - domain: 'host.example', - discoverable: true, - username: 'eligible_remote' - ) - end - - it 'returns the local discoverable account and the remote discoverable account' do - get '/api/v1/directory', headers: headers - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq(2) - expect(body_as_json.pluck(:id)).to contain_exactly(eligible_remote_account.id.to_s, local_discoverable_account.id.to_s) - end - end - - context 'when asking for local accounts only' do - let(:user) { Fabricate(:user, confirmed_at: 10.days.ago, approved: true) } - let(:local_account) { Fabricate(:account, domain: nil, user: user) } - let(:remote_account) { Fabricate(:account, domain: 'host.example') } - - before do - local_account.create_account_stat! - remote_account.create_account_stat! - end - - it 'returns only the local accounts' do - get '/api/v1/directory', headers: headers, params: { local: '1' } - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq(1) - expect(body_as_json.first[:id]).to include(local_account.id.to_s) - expect(response.body).to_not include(remote_account.id.to_s) - end - end - - context 'when ordered by active' do - it 'returns accounts in order of most recent status activity' do - old_stat = Fabricate(:account_stat, last_status_at: 1.day.ago) - new_stat = Fabricate(:account_stat, last_status_at: 1.minute.ago) - - get '/api/v1/directory', headers: headers, params: { order: 'active' } - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq(2) - expect(body_as_json.first[:id]).to include(new_stat.account_id.to_s) - expect(body_as_json.second[:id]).to include(old_stat.account_id.to_s) - end - end - - context 'when ordered by new' do - it 'returns accounts in order of creation' do - account_old = Fabricate(:account_stat).account - travel_to 10.seconds.from_now - account_new = Fabricate(:account_stat).account - - get '/api/v1/directory', headers: headers, params: { order: 'new' } - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq(2) - expect(body_as_json.first[:id]).to include(account_new.id.to_s) - expect(body_as_json.second[:id]).to include(account_old.id.to_s) - end - end - end -end diff --git a/spec/requests/api/v1/domain_blocks_spec.rb b/spec/requests/api/v1/domain_blocks_spec.rb deleted file mode 100644 index 954497ebe1..0000000000 --- a/spec/requests/api/v1/domain_blocks_spec.rb +++ /dev/null @@ -1,110 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Domain blocks' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:blocks write:blocks' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/domain_blocks' do - subject do - get '/api/v1/domain_blocks', headers: headers, params: params - end - - let(:blocked_domains) { ['example.com', 'example.net', 'example.org', 'example.com.br'] } - let(:params) { {} } - - before do - blocked_domains.each { |domain| user.account.block_domain!(domain) } - end - - it_behaves_like 'forbidden for wrong scope', 'write:blocks' - - it 'returns the domains blocked by the requesting user', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to match_array(blocked_domains) - end - - context 'with limit param' do - let(:params) { { limit: 2 } } - - it 'returns only the requested number of blocked domains' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - end - end - - describe 'POST /api/v1/domain_blocks' do - subject do - post '/api/v1/domain_blocks', headers: headers, params: params - end - - let(:params) { { domain: 'example.com' } } - - it_behaves_like 'forbidden for wrong scope', 'read read:blocks' - - it 'creates a domain block', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.domain_blocking?(params[:domain])).to be(true) - end - - context 'when no domain name is given' do - let(:params) { { domain: '' } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the given domain name is invalid' do - let(:params) { { domain: 'example com' } } - - it 'returns unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - end - - describe 'DELETE /api/v1/domain_blocks' do - subject do - delete '/api/v1/domain_blocks/', headers: headers, params: params - end - - let(:params) { { domain: 'example.com' } } - - before do - user.account.block_domain!('example.com') - end - - it_behaves_like 'forbidden for wrong scope', 'read read:blocks' - - it 'deletes the specified domain block', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.domain_blocking?('example.com')).to be(false) - end - - context 'when the given domain name is not blocked' do - let(:params) { { domain: 'example.org' } } - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - end - end -end diff --git a/spec/requests/api/v1/emails/confirmations_spec.rb b/spec/requests/api/v1/emails/confirmations_spec.rb deleted file mode 100644 index 8f5171ee78..0000000000 --- a/spec/requests/api/v1/emails/confirmations_spec.rb +++ /dev/null @@ -1,168 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Confirmations' do - let(:confirmed_at) { nil } - let(:user) { Fabricate(:user, confirmed_at: confirmed_at) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:accounts write:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'POST /api/v1/emails/confirmations' do - subject do - post '/api/v1/emails/confirmations', headers: headers, params: params - end - - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'read read:accounts' - - context 'with an oauth token' do - context 'when user was created by a different application' do - let(:user) { Fabricate(:user, confirmed_at: confirmed_at, created_by_application: Fabricate(:application)) } - - it 'returns http forbidden' do - subject - - expect(response).to have_http_status(403) - end - end - - context 'when user was created by the same application' do - before do - user.update(created_by_application: token.application) - end - - context 'when the account is already confirmed' do - let(:confirmed_at) { Time.now.utc } - - it 'returns http forbidden' do - subject - - expect(response).to have_http_status(403) - end - - context 'when user changed e-mail and has not confirmed it' do - before do - user.update(email: 'foo@bar.com') - end - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - end - end - - context 'when the account is unconfirmed' do - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - end - - context 'with email param' do - let(:params) { { email: 'foo@bar.com' } } - - it "updates the user's e-mail address", :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.reload.unconfirmed_email).to eq('foo@bar.com') - end - end - - context 'with invalid email param' do - let(:params) { { email: 'invalid' } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - end - end - - context 'without an oauth token' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end - - describe 'GET /api/v1/emails/check_confirmation' do - subject do - get '/api/v1/emails/check_confirmation', headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'write' - - context 'with an oauth token' do - context 'when the account is not confirmed' do - it 'returns the confirmation status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to be false - end - end - - context 'when the account is confirmed' do - let(:confirmed_at) { Time.now.utc } - - it 'returns the confirmation status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to be true - end - end - end - - context 'with an authentication cookie' do - let(:headers) { {} } - - before do - sign_in user, scope: :user - end - - context 'when the account is not confirmed' do - it 'returns the confirmation status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to be false - end - end - - context 'when the account is confirmed' do - let(:confirmed_at) { Time.now.utc } - - it 'returns the confirmation status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to be true - end - end - end - - context 'without an oauth token and an authentication cookie' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v1/endorsements_spec.rb b/spec/requests/api/v1/endorsements_spec.rb deleted file mode 100644 index e267f2abd2..0000000000 --- a/spec/requests/api/v1/endorsements_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Endorsements' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/endorsements' do - context 'when not authorized' do - it 'returns http unauthorized' do - get api_v1_endorsements_path - - expect(response) - .to have_http_status(401) - end - end - - context 'with wrong scope' do - before do - get api_v1_endorsements_path, headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'write write:accounts' - end - - context 'with correct scope' do - let(:scopes) { 'read:accounts' } - - context 'with endorsed accounts' do - let!(:account_pin) { Fabricate(:account_pin, account: user.account) } - - it 'returns http success and accounts json' do - get api_v1_endorsements_path, headers: headers - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - .and have_attributes( - first: include(acct: account_pin.target_account.acct) - ) - end - end - - context 'without endorsed accounts without json' do - it 'returns http success' do - get api_v1_endorsements_path, headers: headers - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to_not be_present - end - end - end - end -end diff --git a/spec/requests/api/v1/favourites_spec.rb b/spec/requests/api/v1/favourites_spec.rb deleted file mode 100644 index b988ac99db..0000000000 --- a/spec/requests/api/v1/favourites_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Favourites' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:favourites' } - let(:headers) { { Authorization: "Bearer #{token.token}" } } - - describe 'GET /api/v1/favourites' do - subject do - get '/api/v1/favourites', headers: headers, params: params - end - - let(:params) { {} } - let!(:favourites) { Fabricate.times(2, :favourite, account: user.account) } - - let(:expected_response) do - favourites.map do |favourite| - a_hash_including(id: favourite.status.id.to_s, account: a_hash_including(id: favourite.status.account.id.to_s)) - end - end - - it_behaves_like 'forbidden for wrong scope', 'write' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'returns the favourites' do - subject - - expect(body_as_json).to match_array(expected_response) - end - - context 'with limit param' do - let(:params) { { limit: 1 } } - - it 'returns only the requested number of favourites' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - - it 'sets the correct pagination headers' do - subject - - expect(response) - .to include_pagination_headers( - prev: api_v1_favourites_url(limit: params[:limit], min_id: favourites.last.id), - next: api_v1_favourites_url(limit: params[:limit], max_id: favourites.second.id) - ) - end - end - - context 'without an authorization header' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v1/featured_tags/suggestions_spec.rb b/spec/requests/api/v1/featured_tags/suggestions_spec.rb deleted file mode 100644 index 00451540ca..0000000000 --- a/spec/requests/api/v1/featured_tags/suggestions_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Featured Tags Suggestions API' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:account) { Fabricate(:account, user: user) } - - describe 'GET /api/v1/featured_tags/suggestions' do - let!(:unused_featured_tag) { Fabricate(:tag, name: 'unused_featured_tag') } - let!(:used_tag) { Fabricate(:tag, name: 'used_tag') } - let!(:used_featured_tag) { Fabricate(:tag, name: 'used_featured_tag') } - - before do - _unused_tag = Fabricate(:tag, name: 'unused_tag') - - # Make relevant tags used by account - status = Fabricate(:status, account: account) - status.tags << used_tag - status.tags << used_featured_tag - - # Feature the relevant tags - Fabricate :featured_tag, account: account, name: unused_featured_tag.name - Fabricate :featured_tag, account: account, name: used_featured_tag.name - end - - it 'returns http success and recently used but not featured tags' do - get '/api/v1/featured_tags/suggestions', params: { limit: 2 }, headers: headers - - expect(response) - .to have_http_status(200) - expect(body_as_json) - .to contain_exactly( - include(name: used_tag.name) - ) - end - end -end diff --git a/spec/requests/api/v1/featured_tags_spec.rb b/spec/requests/api/v1/featured_tags_spec.rb deleted file mode 100644 index 4b96988704..0000000000 --- a/spec/requests/api/v1/featured_tags_spec.rb +++ /dev/null @@ -1,193 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'FeaturedTags' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:accounts write:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/featured_tags' do - context 'with wrong scope' do - before do - get '/api/v1/featured_tags', headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'read:statuses' - end - - context 'when Authorization header is missing' do - it 'returns http unauthorized' do - get '/api/v1/featured_tags' - - expect(response).to have_http_status(401) - end - end - - it 'returns http success' do - get '/api/v1/featured_tags', headers: headers - - expect(response).to have_http_status(200) - end - - context 'when the requesting user has no featured tag' do - before { Fabricate(:featured_tag) } - - it 'returns an empty body' do - get '/api/v1/featured_tags', headers: headers - - body = body_as_json - - expect(body).to be_empty - end - end - - context 'when the requesting user has featured tags' do - let!(:user_featured_tags) { Fabricate.times(1, :featured_tag, account: user.account) } - - it 'returns only the featured tags belonging to the requesting user' do - get '/api/v1/featured_tags', headers: headers - - body = body_as_json - expected_ids = user_featured_tags.pluck(:id).map(&:to_s) - - expect(body.pluck(:id)).to match_array(expected_ids) - end - end - end - - describe 'POST /api/v1/featured_tags' do - let(:params) { { name: 'tag' } } - - it 'returns http success' do - post '/api/v1/featured_tags', headers: headers, params: params - - expect(response).to have_http_status(200) - end - - it 'returns the correct tag name' do - post '/api/v1/featured_tags', headers: headers, params: params - - body = body_as_json - - expect(body[:name]).to eq(params[:name]) - end - - it 'creates a new featured tag for the requesting user' do - post '/api/v1/featured_tags', headers: headers, params: params - - featured_tag = FeaturedTag.find_by(name: params[:name], account: user.account) - - expect(featured_tag).to be_present - end - - context 'with wrong scope' do - before do - post '/api/v1/featured_tags', headers: headers, params: params - end - - it_behaves_like 'forbidden for wrong scope', 'read:statuses' - end - - context 'when Authorization header is missing' do - it 'returns http unauthorized' do - post '/api/v1/featured_tags', params: params - - expect(response).to have_http_status(401) - end - end - - context 'when required param "name" is not provided' do - it 'returns http bad request' do - post '/api/v1/featured_tags', headers: headers - - expect(response).to have_http_status(400) - end - end - - context 'when provided tag name is invalid' do - let(:params) { { name: 'asj&*!' } } - - it 'returns http unprocessable entity' do - post '/api/v1/featured_tags', headers: headers, params: params - - expect(response).to have_http_status(422) - end - end - - context 'when tag name is already taken' do - before do - FeaturedTag.create(name: params[:name], account: user.account) - end - - it 'returns http unprocessable entity' do - post '/api/v1/featured_tags', headers: headers, params: params - - expect(response).to have_http_status(422) - end - end - end - - describe 'DELETE /api/v1/featured_tags' do - let!(:featured_tag) { FeaturedTag.create(name: 'tag', account: user.account) } - let(:id) { featured_tag.id } - - it 'returns http success' do - delete "/api/v1/featured_tags/#{id}", headers: headers - - expect(response).to have_http_status(200) - end - - it 'returns an empty body' do - delete "/api/v1/featured_tags/#{id}", headers: headers - - body = body_as_json - - expect(body).to be_empty - end - - it 'deletes the featured tag', :inline_jobs do - delete "/api/v1/featured_tags/#{id}", headers: headers - - featured_tag = FeaturedTag.find_by(id: id) - - expect(featured_tag).to be_nil - end - - context 'with wrong scope' do - before do - delete "/api/v1/featured_tags/#{id}", headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'read:statuses' - end - - context 'when Authorization header is missing' do - it 'returns http unauthorized' do - delete "/api/v1/featured_tags/#{id}" - - expect(response).to have_http_status(401) - end - end - - context 'when featured tag with given id does not exist' do - it 'returns http not found' do - delete '/api/v1/featured_tags/0', headers: headers - - expect(response).to have_http_status(404) - end - end - - context 'when deleting a featured tag of another user' do - let!(:other_user_featured_tag) { Fabricate(:featured_tag) } - let(:id) { other_user_featured_tag.id } - - it 'returns http not found' do - delete "/api/v1/featured_tags/#{id}", headers: headers - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/filters_spec.rb b/spec/requests/api/v1/filters_spec.rb deleted file mode 100644 index deb6e74217..0000000000 --- a/spec/requests/api/v1/filters_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V1 Filters' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/filters' do - let(:scopes) { 'read:filters' } - let!(:filter) { Fabricate(:custom_filter, account: user.account) } - let!(:custom_filter_keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) } - - it 'returns http success' do - get '/api/v1/filters', headers: headers - expect(response).to have_http_status(200) - expect(body_as_json) - .to contain_exactly( - include(id: custom_filter_keyword.id.to_s) - ) - end - end - - describe 'POST /api/v1/filters' do - let(:scopes) { 'write:filters' } - let(:irreversible) { true } - let(:whole_word) { false } - - before do - post '/api/v1/filters', params: { phrase: 'magic', context: %w(home), irreversible: irreversible, whole_word: whole_word }, headers: headers - end - - it 'creates a filter', :aggregate_failures do - filter = user.account.custom_filters.first - - expect(response).to have_http_status(200) - expect(filter).to_not be_nil - expect(filter.keywords.pluck(:keyword, :whole_word)).to eq [['magic', whole_word]] - expect(filter.context).to eq %w(home) - expect(filter.irreversible?).to be irreversible - expect(filter.expires_at).to be_nil - end - - context 'with different parameters' do - let(:irreversible) { false } - let(:whole_word) { true } - - it 'creates a filter', :aggregate_failures do - filter = user.account.custom_filters.first - - expect(response).to have_http_status(200) - expect(filter).to_not be_nil - expect(filter.keywords.pluck(:keyword, :whole_word)).to eq [['magic', whole_word]] - expect(filter.context).to eq %w(home) - expect(filter.irreversible?).to be irreversible - expect(filter.expires_at).to be_nil - end - end - end - - describe 'GET /api/v1/filters/:id' do - let(:scopes) { 'read:filters' } - let(:filter) { Fabricate(:custom_filter, account: user.account) } - let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) } - - it 'returns http success' do - get "/api/v1/filters/#{keyword.id}", headers: headers - - expect(response).to have_http_status(200) - end - end - - describe 'PUT /api/v1/filters/:id' do - let(:scopes) { 'write:filters' } - let(:filter) { Fabricate(:custom_filter, account: user.account) } - let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) } - - before do - put "/api/v1/filters/#{keyword.id}", headers: headers, params: { phrase: 'updated' } - end - - it 'updates the filter', :aggregate_failures do - expect(response).to have_http_status(200) - expect(keyword.reload.phrase).to eq 'updated' - end - end - - describe 'DELETE /api/v1/filters/:id' do - let(:scopes) { 'write:filters' } - let(:filter) { Fabricate(:custom_filter, account: user.account) } - let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) } - - before do - delete "/api/v1/filters/#{keyword.id}", headers: headers - end - - it 'removes the filter', :aggregate_failures do - expect(response).to have_http_status(200) - expect { keyword.reload }.to raise_error ActiveRecord::RecordNotFound - end - end -end diff --git a/spec/requests/api/v1/follow_requests_spec.rb b/spec/requests/api/v1/follow_requests_spec.rb deleted file mode 100644 index a8898ccb3e..0000000000 --- a/spec/requests/api/v1/follow_requests_spec.rb +++ /dev/null @@ -1,94 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Follow requests' do - let(:user) { Fabricate(:user, account_attributes: { locked: true }) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:follows write:follows' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/follow_requests' do - subject do - get '/api/v1/follow_requests', headers: headers, params: params - end - - let(:accounts) { Fabricate.times(2, :account) } - let(:params) { {} } - - let(:expected_response) do - accounts.map do |account| - a_hash_including( - id: account.id.to_s, - username: account.username, - acct: account.acct - ) - end - end - - before do - accounts.each { |account| FollowService.new.call(account, user.account) } - end - - it_behaves_like 'forbidden for wrong scope', 'write write:follows' - - it 'returns the expected content from accounts requesting to follow', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to match_array(expected_response) - end - - context 'with limit param' do - let(:params) { { limit: 1 } } - - it 'returns only the requested number of follow requests' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - end - end - - describe 'POST /api/v1/follow_requests/:account_id/authorize' do - subject do - post "/api/v1/follow_requests/#{follower.id}/authorize", headers: headers - end - - let(:follower) { Fabricate(:account) } - - before do - FollowService.new.call(follower, user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'read read:follows' - - it 'allows the requesting follower to follow', :aggregate_failures do - expect { subject }.to change { follower.following?(user.account) }.from(false).to(true) - expect(response).to have_http_status(200) - expect(body_as_json[:followed_by]).to be true - end - end - - describe 'POST /api/v1/follow_requests/:account_id/reject' do - subject do - post "/api/v1/follow_requests/#{follower.id}/reject", headers: headers - end - - let(:follower) { Fabricate(:account) } - - before do - FollowService.new.call(follower, user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'read read:follows' - - it 'removes the follow request', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(FollowRequest.where(target_account: user.account, account: follower)).to_not exist - expect(body_as_json[:followed_by]).to be false - end - end -end diff --git a/spec/requests/api/v1/followed_tags_spec.rb b/spec/requests/api/v1/followed_tags_spec.rb deleted file mode 100644 index 3d2d82d5db..0000000000 --- a/spec/requests/api/v1/followed_tags_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Followed tags' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:follows' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/followed_tags' do - subject do - get '/api/v1/followed_tags', headers: headers, params: params - end - - let!(:tag_follows) { Fabricate.times(2, :tag_follow, account: user.account) } - let(:params) { {} } - - let(:expected_response) do - tag_follows.map do |tag_follow| - a_hash_including(name: tag_follow.tag.name, following: true) - end - end - - before do - Fabricate(:tag_follow) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:follows' - - it 'returns http success' do - subject - - expect(response).to have_http_status(:success) - end - - it 'returns the followed tags correctly' do - subject - - expect(body_as_json).to match_array(expected_response) - end - - context 'with limit param' do - let(:params) { { limit: 1 } } - - it 'returns only the requested number of follow tags' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - - it 'sets the correct pagination headers' do - subject - - expect(response) - .to include_pagination_headers( - prev: api_v1_followed_tags_url(limit: params[:limit], since_id: tag_follows.last.id), - next: api_v1_followed_tags_url(limit: params[:limit], max_id: tag_follows.last.id) - ) - end - end - end -end diff --git a/spec/requests/api/v1/instance_spec.rb b/spec/requests/api/v1/instance_spec.rb deleted file mode 100644 index 600584eccb..0000000000 --- a/spec/requests/api/v1/instance_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Instances' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/instance' do - context 'when not logged in' do - it 'returns http success and json' do - get api_v1_instance_path - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - .and include(title: 'Mastodon Glitch Edition') - end - end - - context 'when logged in' do - it 'returns http success and json' do - get api_v1_instance_path, headers: headers - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - .and include(title: 'Mastodon Glitch Edition') - end - end - end -end diff --git a/spec/requests/api/v1/instances/activity_spec.rb b/spec/requests/api/v1/instances/activity_spec.rb deleted file mode 100644 index 4f2bc91ad6..0000000000 --- a/spec/requests/api/v1/instances/activity_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Activity' do - describe 'GET /api/v1/instance/activity' do - context 'with activity api enabled' do - before { Setting.activity_api_enabled = true } - - it 'returns http success' do - get api_v1_instance_activity_path - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - .and(be_an(Array)) - .and(have_attributes(size: Api::V1::Instances::ActivityController::WEEKS_OF_ACTIVITY)) - end - end - - context 'with activity api diabled' do - before { Setting.activity_api_enabled = false } - - it 'returns not found' do - get api_v1_instance_activity_path - - expect(response) - .to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/instances/domain_blocks_spec.rb b/spec/requests/api/v1/instances/domain_blocks_spec.rb deleted file mode 100644 index 397ecff084..0000000000 --- a/spec/requests/api/v1/instances/domain_blocks_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Domain Blocks' do - describe 'GET /api/v1/instance/domain_blocks' do - before do - Fabricate(:domain_block) - end - - context 'with domain blocks set to all' do - before { Setting.show_domain_blocks = 'all' } - - it 'returns http success' do - get api_v1_instance_domain_blocks_path - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - .and(be_an(Array)) - .and(have_attributes(size: 1)) - end - end - - context 'with domain blocks set to users' do - before { Setting.show_domain_blocks = 'users' } - - it 'returns http not found' do - get api_v1_instance_domain_blocks_path - - expect(response) - .to have_http_status(404) - end - end - - context 'with domain blocks set to disabled' do - before { Setting.show_domain_blocks = 'disabled' } - - it 'returns http not found' do - get api_v1_instance_domain_blocks_path - - expect(response) - .to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/instances/extended_descriptions_spec.rb b/spec/requests/api/v1/instances/extended_descriptions_spec.rb deleted file mode 100644 index 64982de686..0000000000 --- a/spec/requests/api/v1/instances/extended_descriptions_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Extended Descriptions' do - describe 'GET /api/v1/instance/extended_description' do - it 'returns http success' do - get api_v1_instance_extended_description_path - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - .and include(:content) - end - end -end diff --git a/spec/requests/api/v1/instances/languages_spec.rb b/spec/requests/api/v1/instances/languages_spec.rb deleted file mode 100644 index 8ab8bf99ce..0000000000 --- a/spec/requests/api/v1/instances/languages_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Languages' do - describe 'GET /api/v1/instance/languages' do - before do - get '/api/v1/instance/languages' - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'returns the supported languages' do - expect(body_as_json.pluck(:code)).to match_array LanguagesHelper::SUPPORTED_LOCALES.keys.map(&:to_s) - end - end -end diff --git a/spec/requests/api/v1/instances/peers_spec.rb b/spec/requests/api/v1/instances/peers_spec.rb deleted file mode 100644 index 1a7975f8b7..0000000000 --- a/spec/requests/api/v1/instances/peers_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Peers' do - describe 'GET /api/v1/instance/peers' do - context 'with peers api enabled' do - before { Setting.peers_api_enabled = true } - - it 'returns http success' do - get api_v1_instance_peers_path - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_an(Array) - end - end - - context 'with peers api diabled' do - before { Setting.peers_api_enabled = false } - - it 'returns http not found' do - get api_v1_instance_peers_path - - expect(response) - .to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/instances/privacy_policies_spec.rb b/spec/requests/api/v1/instances/privacy_policies_spec.rb deleted file mode 100644 index 24de98d880..0000000000 --- a/spec/requests/api/v1/instances/privacy_policies_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Privacy Policy' do - describe 'GET /api/v1/instance/privacy_policy' do - it 'returns http success' do - get api_v1_instance_privacy_policy_path - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - .and include(:content) - end - end -end diff --git a/spec/requests/api/v1/instances/rules_spec.rb b/spec/requests/api/v1/instances/rules_spec.rb deleted file mode 100644 index 65b8d78c7d..0000000000 --- a/spec/requests/api/v1/instances/rules_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Rules' do - describe 'GET /api/v1/instance/rules' do - it 'returns http success' do - get api_v1_instance_rules_path - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_an(Array) - end - end -end diff --git a/spec/requests/api/v1/instances/translation_languages_spec.rb b/spec/requests/api/v1/instances/translation_languages_spec.rb deleted file mode 100644 index 0b7dd8314d..0000000000 --- a/spec/requests/api/v1/instances/translation_languages_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Translation Languages' do - describe 'GET /api/v1/instances/translation_languages' do - context 'when no translation service is configured' do - it 'returns empty language matrix', :aggregate_failures do - get api_v1_instance_translation_languages_path - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to eq({}) - end - end - - context 'when a translation service is configured' do - before { configure_translation_service } - - it 'returns language matrix', :aggregate_failures do - get api_v1_instance_translation_languages_path - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to eq({ und: %w(en de), en: ['de'] }) - end - - private - - def configure_translation_service - allow(TranslationService).to receive_messages(configured?: true, configured: service_double) - end - - def service_double - instance_double(TranslationService::DeepL, languages: { nil => %w(en de), 'en' => ['de'] }) - end - end - end -end diff --git a/spec/requests/api/v1/lists/accounts_spec.rb b/spec/requests/api/v1/lists/accounts_spec.rb deleted file mode 100644 index de49982351..0000000000 --- a/spec/requests/api/v1/lists/accounts_spec.rb +++ /dev/null @@ -1,178 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Accounts' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:lists write:lists' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/lists/:id/accounts' do - subject do - get "/api/v1/lists/#{list.id}/accounts", headers: headers, params: params - end - - let(:params) { { limit: 0 } } - let(:list) { Fabricate(:list, account: user.account) } - let(:accounts) { Fabricate.times(2, :account) } - - let(:expected_response) do - accounts.map do |account| - a_hash_including(id: account.id.to_s, username: account.username, acct: account.acct) - end - end - - before do - accounts.each { |account| user.account.follow!(account) } - list.accounts << accounts - end - - it_behaves_like 'forbidden for wrong scope', 'write write:lists' - - it 'returns the accounts in the requested list', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to match_array(expected_response) - end - - context 'with limit param' do - let(:params) { { limit: 1 } } - - it 'returns only the requested number of accounts' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - end - end - - describe 'POST /api/v1/lists/:id/accounts' do - subject do - post "/api/v1/lists/#{list.id}/accounts", headers: headers, params: params - end - - let(:list) { Fabricate(:list, account: user.account) } - let(:bob) { Fabricate(:account, username: 'bob') } - let(:params) { { account_ids: [bob.id] } } - - it_behaves_like 'forbidden for wrong scope', 'read read:lists' - - context 'when the added account is followed' do - before do - user.account.follow!(bob) - end - - it 'adds account to the list', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(list.accounts).to include(bob) - end - end - - context 'when the added account has been sent a follow request' do - before do - user.account.follow_requests.create!(target_account: bob) - end - - it 'adds account to the list', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(list.accounts).to include(bob) - end - end - - context 'when the added account is not followed' do - it 'does not add the account to the list', :aggregate_failures do - subject - - expect(response).to have_http_status(404) - expect(list.accounts).to_not include(bob) - end - end - - context 'when the list is not owned by the requesting user' do - let(:list) { Fabricate(:list) } - - before do - user.account.follow!(bob) - end - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when account is already in the list' do - before do - user.account.follow!(bob) - list.accounts << bob - end - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - end - - describe 'DELETE /api/v1/lists/:id/accounts' do - subject do - delete "/api/v1/lists/#{list.id}/accounts", headers: headers, params: params - end - - context 'when the list is owned by the requesting user' do - let(:list) { Fabricate(:list, account: user.account) } - let(:bob) { Fabricate(:account, username: 'bob') } - let(:peter) { Fabricate(:account, username: 'peter') } - let(:params) { { account_ids: [bob.id] } } - - before do - user.account.follow!(bob) - user.account.follow!(peter) - list.accounts << [bob, peter] - end - - it 'removes the specified account from the list', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(list.accounts).to_not include(bob) - end - - it 'does not remove any other account from the list' do - subject - - expect(list.accounts).to include(peter) - end - - context 'when the specified account is not in the list' do - let(:params) { { account_ids: [0] } } - - it 'does not remove any account from the list', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(list.accounts).to contain_exactly(bob, peter) - end - end - end - - context 'when the list is not owned by the requesting user' do - let(:list) { Fabricate(:list) } - let(:params) { {} } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/lists_spec.rb b/spec/requests/api/v1/lists_spec.rb deleted file mode 100644 index 4635e936f5..0000000000 --- a/spec/requests/api/v1/lists_spec.rb +++ /dev/null @@ -1,220 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Lists' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:lists write:lists' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/lists' do - subject do - get '/api/v1/lists', headers: headers - end - - let!(:lists) do - [ - Fabricate(:list, account: user.account, title: 'first list', replies_policy: :followed), - Fabricate(:list, account: user.account, title: 'second list', replies_policy: :list), - Fabricate(:list, account: user.account, title: 'third list', replies_policy: :none), - Fabricate(:list, account: user.account, title: 'fourth list', exclusive: true), - ] - end - - let(:expected_response) do - lists.map do |list| - { - id: list.id.to_s, - title: list.title, - replies_policy: list.replies_policy, - exclusive: list.exclusive, - } - end - end - - before do - Fabricate(:list) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:lists' - - it 'returns the expected lists', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to match_array(expected_response) - end - end - - describe 'GET /api/v1/lists/:id' do - subject do - get "/api/v1/lists/#{list.id}", headers: headers - end - - let(:list) { Fabricate(:list, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'write write:lists' - - it 'returns the requested list correctly', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to eq({ - id: list.id.to_s, - title: list.title, - replies_policy: list.replies_policy, - exclusive: list.exclusive, - }) - end - - context 'when the list belongs to a different user' do - let(:list) { Fabricate(:list) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when the list does not exist' do - it 'returns http not found' do - get '/api/v1/lists/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/lists' do - subject do - post '/api/v1/lists', headers: headers, params: params - end - - let(:params) { { title: 'my list', replies_policy: 'none', exclusive: 'true' } } - - it_behaves_like 'forbidden for wrong scope', 'read read:lists' - - it 'returns the new list', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to match(a_hash_including(title: 'my list', replies_policy: 'none', exclusive: true)) - expect(List.where(account: user.account).count).to eq(1) - end - - context 'when a title is not given' do - let(:params) { { title: '' } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the given replies_policy is invalid' do - let(:params) { { title: 'a list', replies_policy: 'whatever' } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - end - - describe 'PUT /api/v1/lists/:id' do - subject do - put "/api/v1/lists/#{list.id}", headers: headers, params: params - end - - let(:list) { Fabricate(:list, account: user.account, title: 'my list') } - let(:params) { { title: 'list', replies_policy: 'followed', exclusive: 'true' } } - - it_behaves_like 'forbidden for wrong scope', 'read read:lists' - - it 'returns the updated list and updates values', :aggregate_failures do - expect { subject } - .to change_list_title - .and change_list_replies_policy - .and change_list_exclusive - - expect(response).to have_http_status(200) - list.reload - - expect(body_as_json).to eq({ - id: list.id.to_s, - title: list.title, - replies_policy: list.replies_policy, - exclusive: list.exclusive, - }) - end - - def change_list_title - change { list.reload.title }.from('my list').to('list') - end - - def change_list_replies_policy - change { list.reload.replies_policy }.from('list').to('followed') - end - - def change_list_exclusive - change { list.reload.exclusive }.from(false).to(true) - end - - context 'when the list does not exist' do - it 'returns http not found' do - put '/api/v1/lists/-1', headers: headers, params: params - - expect(response).to have_http_status(404) - end - end - - context 'when the list belongs to another user' do - let(:list) { Fabricate(:list) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'DELETE /api/v1/lists/:id' do - subject do - delete "/api/v1/lists/#{list.id}", headers: headers - end - - let(:list) { Fabricate(:list, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'read read:lists' - - it 'deletes the list', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(List.where(id: list.id)).to_not exist - end - - context 'when the list does not exist' do - it 'returns http not found' do - delete '/api/v1/lists/-1', headers: headers - - expect(response).to have_http_status(404) - end - end - - context 'when the list belongs to another user' do - let(:list) { Fabricate(:list) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/markers_spec.rb b/spec/requests/api/v1/markers_spec.rb deleted file mode 100644 index b04adf2594..0000000000 --- a/spec/requests/api/v1/markers_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API Markers' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:statuses write:statuses' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/markers' do - before do - Fabricate(:marker, timeline: 'home', last_read_id: 123, user: user) - Fabricate(:marker, timeline: 'notifications', last_read_id: 456, user: user) - - get '/api/v1/markers', headers: headers, params: { timeline: %w(home notifications) } - end - - it 'returns markers', :aggregate_failures do - json = body_as_json - - expect(response).to have_http_status(200) - expect(json.key?(:home)).to be true - expect(json[:home][:last_read_id]).to eq '123' - expect(json.key?(:notifications)).to be true - expect(json[:notifications][:last_read_id]).to eq '456' - end - end - - describe 'POST /api/v1/markers' do - context 'when no marker exists' do - before do - post '/api/v1/markers', headers: headers, params: { home: { last_read_id: '69420' } } - end - - it 'creates a marker', :aggregate_failures do - expect(response).to have_http_status(200) - expect(user.markers.first.timeline).to eq 'home' - expect(user.markers.first.last_read_id).to eq 69_420 - end - end - - context 'when a marker exists' do - before do - post '/api/v1/markers', headers: headers, params: { home: { last_read_id: '69420' } } - post '/api/v1/markers', headers: headers, params: { home: { last_read_id: '70120' } } - end - - it 'updates a marker', :aggregate_failures do - expect(response).to have_http_status(200) - expect(user.markers.first.timeline).to eq 'home' - expect(user.markers.first.last_read_id).to eq 70_120 - end - end - - context 'when database object becomes stale' do - before do - allow(Marker).to receive(:transaction).and_raise(ActiveRecord::StaleObjectError) - post '/api/v1/markers', headers: headers, params: { home: { last_read_id: '69420' } } - end - - it 'returns error json' do - expect(response) - .to have_http_status(409) - expect(body_as_json) - .to include(error: /Conflict during update/) - end - end - end -end diff --git a/spec/requests/api/v1/media_spec.rb b/spec/requests/api/v1/media_spec.rb deleted file mode 100644 index c89c49afdf..0000000000 --- a/spec/requests/api/v1/media_spec.rb +++ /dev/null @@ -1,183 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Media' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'write:media' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/media/:id' do - subject do - get "/api/v1/media/#{media.id}", headers: headers - end - - let(:media) { Fabricate(:media_attachment, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'read' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'returns the media information' do - subject - - expect(body_as_json).to match( - a_hash_including( - id: media.id.to_s, - description: media.description, - type: media.type - ) - ) - end - - context 'when the media is still being processed' do - before do - media.update(processing: :in_progress) - end - - it 'returns http partial content' do - subject - - expect(response).to have_http_status(206) - end - end - - context 'when the media belongs to somebody else' do - let(:media) { Fabricate(:media_attachment) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when media is attached to a status' do - let(:media) { Fabricate(:media_attachment, account: user.account, status: Fabricate.build(:status)) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/media' do - subject do - post '/api/v1/media', headers: headers, params: params - end - - let(:params) { {} } - - shared_examples 'a successful media upload' do |media_type| - it 'uploads the file successfully and returns correct media content', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(MediaAttachment.first).to be_present - expect(MediaAttachment.first).to have_attached_file(:file) - - expect(body_as_json).to match( - a_hash_including(id: MediaAttachment.first.id.to_s, description: params[:description], type: media_type) - ) - end - end - - it_behaves_like 'forbidden for wrong scope', 'read read:media' - - describe 'when paperclip errors occur' do - let(:media_attachments) { double } - let(:params) { { file: fixture_file_upload('attachment.jpg', 'image/jpeg') } } - - before do - allow(User).to receive(:find).with(token.resource_owner_id).and_return(user) - allow(user.account).to receive(:media_attachments).and_return(media_attachments) - end - - context 'when imagemagick cannot identify the file type' do - it 'returns http unprocessable entity' do - allow(media_attachments).to receive(:create!).and_raise(Paperclip::Errors::NotIdentifiedByImageMagickError) - - subject - - expect(response).to have_http_status(422) - end - end - - context 'when there is a generic error' do - it 'returns http 500' do - allow(media_attachments).to receive(:create!).and_raise(Paperclip::Error) - - subject - - expect(response).to have_http_status(500) - end - end - end - - context 'with image/jpeg', :attachment_processing do - let(:params) { { file: fixture_file_upload('attachment.jpg', 'image/jpeg'), description: 'jpeg image' } } - - it_behaves_like 'a successful media upload', 'image' - end - - context 'with image/gif', :attachment_processing do - let(:params) { { file: fixture_file_upload('attachment.gif', 'image/gif') } } - - it_behaves_like 'a successful media upload', 'image' - end - - context 'with video/webm', :attachment_processing do - let(:params) { { file: fixture_file_upload('attachment.webm', 'video/webm') } } - - it_behaves_like 'a successful media upload', 'gifv' - end - end - - describe 'PUT /api/v1/media/:id' do - subject do - put "/api/v1/media/#{media.id}", headers: headers, params: params - end - - let(:params) { {} } - let(:media) { Fabricate(:media_attachment, status: status, account: user.account, description: 'old') } - - it_behaves_like 'forbidden for wrong scope', 'read read:media' - - context 'when the media belongs to somebody else' do - let(:media) { Fabricate(:media_attachment, status: nil) } - let(:params) { { description: 'Lorem ipsum!!!' } } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when the requesting user owns the media' do - let(:status) { nil } - let(:params) { { description: 'Lorem ipsum!!!' } } - - it 'updates the description' do - expect { subject }.to change { media.reload.description }.from('old').to('Lorem ipsum!!!') - end - - context 'when the media is attached to a status' do - let(:status) { Fabricate(:status, account: user.account) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - end -end diff --git a/spec/requests/api/v1/mutes_spec.rb b/spec/requests/api/v1/mutes_spec.rb deleted file mode 100644 index 019bf16584..0000000000 --- a/spec/requests/api/v1/mutes_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Mutes' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:mutes' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/mutes' do - subject do - get '/api/v1/mutes', headers: headers, params: params - end - - let!(:mutes) { Fabricate.times(2, :mute, account: user.account) } - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'write write:mutes' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'returns the muted accounts' do - subject - - muted_accounts = mutes.map(&:target_account) - - expect(body_as_json.pluck(:id)).to match_array(muted_accounts.map { |account| account.id.to_s }) - end - - context 'with limit param' do - let(:params) { { limit: 1 } } - - it 'returns only the requested number of muted accounts' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - - it 'sets the correct pagination headers', :aggregate_failures do - subject - - expect(response) - .to include_pagination_headers( - prev: api_v1_mutes_url(limit: params[:limit], since_id: mutes.last.id), - next: api_v1_mutes_url(limit: params[:limit], max_id: mutes.last.id) - ) - end - end - - context 'with max_id param' do - let(:params) { { max_id: mutes[1].id } } - - it 'queries mutes in range according to max_id', :aggregate_failures do - subject - - body = body_as_json - - expect(body.size).to eq 1 - expect(body[0][:id]).to eq mutes[0].target_account_id.to_s - end - end - - context 'with since_id param' do - let(:params) { { since_id: mutes[0].id } } - - it 'queries mutes in range according to since_id', :aggregate_failures do - subject - - body = body_as_json - - expect(body.size).to eq 1 - expect(body[0][:id]).to eq mutes[1].target_account_id.to_s - end - end - - context 'without an authentication header' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v1/notifications/policies_spec.rb b/spec/requests/api/v1/notifications/policies_spec.rb deleted file mode 100644 index cbd4499772..0000000000 --- a/spec/requests/api/v1/notifications/policies_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Policies' do - let(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:notifications write:notifications' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/notifications/policy', :inline_jobs do - subject do - get '/api/v1/notifications/policy', headers: headers, params: params - end - - let(:params) { {} } - - before do - Fabricate(:notification_request, account: user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:notifications' - - context 'with no options' do - it 'returns json with expected attributes', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to include( - filter_not_following: false, - filter_not_followers: false, - filter_new_accounts: false, - filter_private_mentions: true, - summary: a_hash_including( - pending_requests_count: 1, - pending_notifications_count: 0 - ) - ) - end - end - end - - describe 'PUT /api/v1/notifications/policy' do - subject do - put '/api/v1/notifications/policy', headers: headers, params: params - end - - let(:params) { { filter_not_following: true } } - - it_behaves_like 'forbidden for wrong scope', 'read read:notifications' - - it 'changes notification policy and returns an updated json object', :aggregate_failures do - expect { subject } - .to change { NotificationPolicy.find_or_initialize_by(account: user.account).filter_not_following }.from(false).to(true) - - expect(response).to have_http_status(200) - expect(body_as_json).to include( - filter_not_following: true, - filter_not_followers: false, - filter_new_accounts: false, - filter_private_mentions: true, - summary: a_hash_including( - pending_requests_count: 0, - pending_notifications_count: 0 - ) - ) - end - end -end diff --git a/spec/requests/api/v1/notifications/requests_spec.rb b/spec/requests/api/v1/notifications/requests_spec.rb deleted file mode 100644 index e1fe17426a..0000000000 --- a/spec/requests/api/v1/notifications/requests_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Requests' do - let(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:notifications write:notifications' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/notifications/requests', :inline_jobs do - subject do - get '/api/v1/notifications/requests', headers: headers, params: params - end - - let(:params) { {} } - - before do - Fabricate(:notification_request, account: user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:notifications' - - context 'with no options' do - it 'returns http success', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - end - end - end - - describe 'POST /api/v1/notifications/requests/:id/accept' do - subject do - post "/api/v1/notifications/requests/#{notification_request.id}/accept", headers: headers - end - - let(:notification_request) { Fabricate(:notification_request, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'read read:notifications' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'creates notification permission' do - subject - - expect(NotificationPermission.find_by(account: notification_request.account, from_account: notification_request.from_account)).to_not be_nil - end - - context 'when notification request belongs to someone else' do - let(:notification_request) { Fabricate(:notification_request) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/notifications/requests/:id/dismiss' do - subject do - post "/api/v1/notifications/requests/#{notification_request.id}/dismiss", headers: headers - end - - let!(:notification_request) { Fabricate(:notification_request, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'read read:notifications' - - it 'returns http success and destroys the notification request', :aggregate_failures do - expect { subject }.to change(NotificationRequest, :count).by(-1) - - expect(response).to have_http_status(200) - end - - context 'when notification request belongs to someone else' do - let(:notification_request) { Fabricate(:notification_request) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/notifications/requests/accept' do - subject do - post '/api/v1/notifications/requests/accept', params: { id: [notification_request.id] }, headers: headers - end - - let!(:notification_request) { Fabricate(:notification_request, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'read read:notifications' - - it 'returns http success and creates notification permission', :aggregate_failures do - subject - - expect(NotificationPermission.find_by(account: notification_request.account, from_account: notification_request.from_account)).to_not be_nil - expect(response).to have_http_status(200) - end - end - - describe 'POST /api/v1/notifications/requests/dismiss' do - subject do - post '/api/v1/notifications/requests/dismiss', params: { id: [notification_request.id] }, headers: headers - end - - let!(:notification_request) { Fabricate(:notification_request, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'read read:notifications' - - it 'returns http success and destroys the notification request', :aggregate_failures do - expect { subject }.to change(NotificationRequest, :count).by(-1) - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/v1/notifications_spec.rb b/spec/requests/api/v1/notifications_spec.rb deleted file mode 100644 index 3d1e8a4787..0000000000 --- a/spec/requests/api/v1/notifications_spec.rb +++ /dev/null @@ -1,277 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Notifications' do - let(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:notifications write:notifications' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/notifications/unread_count', :inline_jobs do - subject do - get '/api/v1/notifications/unread_count', headers: headers, params: params - end - - let(:params) { {} } - - before do - first_status = PostStatusService.new.call(user.account, text: 'Test') - ReblogService.new.call(Fabricate(:account), first_status) - PostStatusService.new.call(Fabricate(:account), text: 'Hello @alice') - FavouriteService.new.call(Fabricate(:account), first_status) - FavouriteService.new.call(Fabricate(:account), first_status) - FollowService.new.call(Fabricate(:account), user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:notifications' - - context 'with no options' do - it 'returns expected notifications count' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:count]).to eq 5 - end - end - - context 'with a read marker' do - before do - id = user.account.notifications.browserable.order(id: :desc).offset(2).first.id - user.markers.create!(timeline: 'notifications', last_read_id: id) - end - - it 'returns expected notifications count' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:count]).to eq 2 - end - end - - context 'with exclude_types param' do - let(:params) { { exclude_types: %w(mention) } } - - it 'returns expected notifications count' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:count]).to eq 4 - end - end - - context 'with a user-provided limit' do - let(:params) { { limit: 2 } } - - it 'returns a capped value' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:count]).to eq 2 - end - end - - context 'when there are more notifications than the limit' do - before do - stub_const('Api::V1::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT', 2) - end - - it 'returns a capped value' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:count]).to eq Api::V1::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT - end - end - end - - describe 'GET /api/v1/notifications', :inline_jobs do - subject do - get '/api/v1/notifications', headers: headers, params: params - end - - let(:bob) { Fabricate(:user) } - let(:tom) { Fabricate(:user) } - let(:params) { {} } - - before do - first_status = PostStatusService.new.call(user.account, text: 'Test') - ReblogService.new.call(bob.account, first_status) - PostStatusService.new.call(bob.account, text: 'Hello @alice') - PostStatusService.new.call(tom.account, text: 'Hello @alice', visibility: :direct) # Filtered by default - FavouriteService.new.call(bob.account, first_status) - FavouriteService.new.call(tom.account, first_status) - FollowService.new.call(bob.account, user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:notifications' - - context 'with no options' do - it 'returns expected notification types', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq 5 - expect(body_json_types).to include('reblog', 'mention', 'favourite', 'follow') - expect(body_as_json.any? { |x| x[:filtered] }).to be false - end - end - - context 'with include_filtered' do - let(:params) { { include_filtered: true } } - - it 'returns expected notification types, including filtered notifications' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq 6 - expect(body_json_types).to include('reblog', 'mention', 'favourite', 'follow') - expect(body_as_json.any? { |x| x[:filtered] }).to be true - end - end - - context 'with account_id param' do - let(:params) { { account_id: tom.account.id } } - - it 'returns only notifications from specified user', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_json_account_ids.uniq).to eq [tom.account.id.to_s] - end - - def body_json_account_ids - body_as_json.map { |x| x[:account][:id] } - end - end - - context 'with invalid account_id param' do - let(:params) { { account_id: 'foo' } } - - it 'returns nothing', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq 0 - end - end - - context 'with exclude_types param' do - let(:params) { { exclude_types: %w(mention) } } - - it 'returns everything but excluded type', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.size).to_not eq 0 - expect(body_json_types.uniq).to_not include 'mention' - end - end - - context 'with types param' do - let(:params) { { types: %w(mention) } } - - it 'returns only requested type', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_json_types.uniq).to eq ['mention'] - end - end - - context 'with limit param' do - let(:params) { { limit: 3 } } - - it 'returns the requested number of notifications paginated', :aggregate_failures do - subject - - notifications = user.account.notifications.browserable - - expect(body_as_json.size) - .to eq(params[:limit]) - - expect(response) - .to include_pagination_headers( - prev: api_v1_notifications_url(limit: params[:limit], min_id: notifications.last.id), - next: api_v1_notifications_url(limit: params[:limit], max_id: notifications[2].id) - ) - end - end - - def body_json_types - body_as_json.pluck(:type) - end - end - - describe 'GET /api/v1/notifications/:id' do - subject do - get "/api/v1/notifications/#{notification.id}", headers: headers - end - - let(:notification) { Fabricate(:notification, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'write write:notifications' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - context 'when notification belongs to someone else' do - let(:notification) { Fabricate(:notification) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/notifications/:id/dismiss' do - subject do - post "/api/v1/notifications/#{notification.id}/dismiss", headers: headers - end - - let!(:notification) { Fabricate(:notification, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'read read:notifications' - - it 'destroys the notification' do - subject - - expect(response).to have_http_status(200) - expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - context 'when notification belongs to someone else' do - let(:notification) { Fabricate(:notification) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/notifications/clear' do - subject do - post '/api/v1/notifications/clear', headers: headers - end - - before do - Fabricate(:notification, account: user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'read read:notifications' - - it 'clears notifications for the account' do - subject - - expect(user.account.reload.notifications).to be_empty - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/v1/peers/search_spec.rb b/spec/requests/api/v1/peers/search_spec.rb deleted file mode 100644 index dcdea387a5..0000000000 --- a/spec/requests/api/v1/peers/search_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API Peers Search' do - describe 'GET /api/v1/peers/search' do - context 'when peers api is disabled' do - before do - Setting.peers_api_enabled = false - end - - it 'returns http not found response' do - get '/api/v1/peers/search' - - expect(response) - .to have_http_status(404) - end - end - - context 'with no search param' do - it 'returns http success and empty response' do - get '/api/v1/peers/search' - - expect(response) - .to have_http_status(200) - expect(body_as_json) - .to be_blank - end - end - - context 'with invalid search param' do - it 'returns http success and empty response' do - get '/api/v1/peers/search', params: { q: 'ftp://Invalid-Host!!.valüe' } - - expect(response) - .to have_http_status(200) - expect(body_as_json) - .to be_blank - end - end - - context 'with search param' do - let!(:account) { Fabricate(:account, domain: 'host.example') } - - before { Instance.refresh } - - it 'returns http success and json with known domains' do - get '/api/v1/peers/search', params: { q: 'host.example' } - - expect(response) - .to have_http_status(200) - expect(body_as_json.size) - .to eq(1) - expect(body_as_json.first) - .to eq(account.domain) - end - end - end -end diff --git a/spec/requests/api/v1/polls/votes_spec.rb b/spec/requests/api/v1/polls/votes_spec.rb deleted file mode 100644 index 669f64b6e4..0000000000 --- a/spec/requests/api/v1/polls/votes_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V1 Polls Votes' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'write:statuses' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'POST /api/v1/polls/:poll_id/votes' do - let(:poll) { Fabricate(:poll) } - let(:params) { { choices: %w(1) } } - - before do - post "/api/v1/polls/#{poll.id}/votes", params: params, headers: headers - end - - it 'creates a vote', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(vote).to_not be_nil - expect(vote.choice).to eq 1 - - expect(poll.reload.cached_tallies).to eq [0, 1] - end - - context 'when the required choices param is not provided' do - let(:params) { {} } - - it 'returns http bad request' do - expect(response).to have_http_status(400) - end - end - - private - - def vote - poll.votes.where(account: user.account).first - end - end -end diff --git a/spec/requests/api/v1/polls_spec.rb b/spec/requests/api/v1/polls_spec.rb deleted file mode 100644 index 1c8a818d59..0000000000 --- a/spec/requests/api/v1/polls_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Polls' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:statuses' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/polls/:id' do - subject do - get "/api/v1/polls/#{poll.id}", headers: headers - end - - let(:poll) { Fabricate(:poll, status: Fabricate(:status, visibility: visibility)) } - let(:visibility) { 'public' } - - it_behaves_like 'forbidden for wrong scope', 'write write:statuses' - - context 'when parent status is public' do - it 'returns the poll data successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to match( - a_hash_including( - id: poll.id.to_s, - voted: false, - voters_count: poll.voters_count, - votes_count: poll.votes_count - ) - ) - end - end - - context 'when parent status is private' do - let(:visibility) { 'private' } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/preferences_spec.rb b/spec/requests/api/v1/preferences_spec.rb deleted file mode 100644 index 6f4188c35a..0000000000 --- a/spec/requests/api/v1/preferences_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Preferences' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/preferences' do - context 'when not authorized' do - it 'returns http unauthorized' do - get api_v1_preferences_path - - expect(response) - .to have_http_status(401) - end - end - - context 'with wrong scope' do - before do - get api_v1_preferences_path, headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'write write:accounts' - end - - context 'with correct scope' do - let(:scopes) { 'read:accounts' } - - it 'returns http success' do - get api_v1_preferences_path, headers: headers - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - end - end - end -end diff --git a/spec/requests/api/v1/profiles_spec.rb b/spec/requests/api/v1/profiles_spec.rb deleted file mode 100644 index 26a9b848e5..0000000000 --- a/spec/requests/api/v1/profiles_spec.rb +++ /dev/null @@ -1,98 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Deleting profile images' do - let(:account) do - Fabricate( - :account, - avatar: fixture_file_upload('avatar.gif', 'image/gif'), - header: fixture_file_upload('attachment.jpg', 'image/jpeg') - ) - end - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: account.user.id, scopes: scopes) } - let(:scopes) { 'write:accounts' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'DELETE /api/v1/profile' do - before do - allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) - end - - context 'when deleting an avatar' do - context 'with wrong scope' do - before do - delete '/api/v1/profile/avatar', headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'read' - end - - it 'returns http success' do - delete '/api/v1/profile/avatar', headers: headers - - expect(response).to have_http_status(200) - end - - it 'deletes the avatar' do - delete '/api/v1/profile/avatar', headers: headers - - account.reload - - expect(account.avatar).to_not exist - end - - it 'does not delete the header' do - delete '/api/v1/profile/avatar', headers: headers - - account.reload - - expect(account.header).to exist - end - - it 'queues up an account update distribution' do - delete '/api/v1/profile/avatar', headers: headers - - expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id) - end - end - - context 'when deleting a header' do - context 'with wrong scope' do - before do - delete '/api/v1/profile/header', headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'read' - end - - it 'returns http success' do - delete '/api/v1/profile/header', headers: headers - - expect(response).to have_http_status(200) - end - - it 'does not delete the avatar' do - delete '/api/v1/profile/header', headers: headers - - account.reload - - expect(account.avatar).to exist - end - - it 'deletes the header' do - delete '/api/v1/profile/header', headers: headers - - account.reload - - expect(account.header).to_not exist - end - - it 'queues up an account update distribution' do - delete '/api/v1/profile/header', headers: headers - - expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id) - end - end - end -end diff --git a/spec/requests/api/v1/push/subscriptions_spec.rb b/spec/requests/api/v1/push/subscriptions_spec.rb deleted file mode 100644 index 54ef5a13ad..0000000000 --- a/spec/requests/api/v1/push/subscriptions_spec.rb +++ /dev/null @@ -1,166 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Push Subscriptions' do - let(:user) { Fabricate(:user) } - let(:endpoint) { 'https://fcm.googleapis.com/fcm/send/fiuH06a27qE:APA91bHnSiGcLwdaxdyqVXNDR9w1NlztsHb6lyt5WDKOC_Z_Q8BlFxQoR8tWFSXUIDdkyw0EdvxTu63iqamSaqVSevW5LfoFwojws8XYDXv_NRRLH6vo2CdgiN4jgHv5VLt2A8ah6lUX' } - let(:keys) do - { - p256dh: 'BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=', - auth: 'eH_C8rq2raXqlcBVDa1gLg==', - } - end - let(:create_payload) do - { - subscription: { - endpoint: endpoint, - keys: keys, - }, - }.with_indifferent_access - end - let(:alerts_payload) do - { - data: { - policy: 'all', - - alerts: { - follow: true, - follow_request: true, - favourite: false, - reblog: true, - mention: false, - poll: true, - status: false, - }, - }, - }.with_indifferent_access - end - let(:scopes) { 'push' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - shared_examples 'validation error' do - it 'returns a validation error' do - subject - - expect(response).to have_http_status(422) - expect(endpoint_push_subscriptions.count).to eq(0) - expect(endpoint_push_subscription).to be_nil - end - end - - describe 'POST /api/v1/push/subscription' do - subject { post '/api/v1/push/subscription', params: create_payload, headers: headers } - - it 'saves push subscriptions and returns expected JSON' do - subject - - expect(endpoint_push_subscription) - .to have_attributes( - endpoint: eq(create_payload[:subscription][:endpoint]), - key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]), - key_auth: eq(create_payload[:subscription][:keys][:auth]), - user_id: eq(user.id), - access_token_id: eq(token.id) - ) - - expect(body_as_json.with_indifferent_access) - .to include( - { endpoint: create_payload[:subscription][:endpoint], alerts: {}, policy: 'all' } - ) - end - - it 'replaces old subscription on repeat calls' do - 2.times { subject } - - expect(endpoint_push_subscriptions.count) - .to eq(1) - end - - context 'with invalid endpoint URL' do - let(:endpoint) { 'app://example.foo' } - - it_behaves_like 'validation error' - end - - context 'with invalid p256dh key' do - let(:keys) do - { - p256dh: 'BEm_invalidf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=', - auth: 'eH_C8rq2raXqlcBVDa1gLg==', - } - end - - it_behaves_like 'validation error' - end - - context 'with invalid base64 p256dh key' do - let(:keys) do - { - p256dh: 'not base64', - auth: 'eH_C8rq2raXqlcBVDa1gLg==', - } - end - - it_behaves_like 'validation error' - end - end - - describe 'PUT /api/v1/push/subscription' do - subject { put '/api/v1/push/subscription', params: alerts_payload, headers: headers } - - before { create_subscription_with_token } - - it 'changes data policy and alert settings and returns expected JSON' do - expect { subject } - .to change { endpoint_push_subscription.reload.data } - .from(nil) - .to(include('policy' => alerts_payload[:data][:policy])) - - %w(follow follow_request favourite reblog mention poll status).each do |type| - expect(endpoint_push_subscription.data['alerts']).to include( - type.to_s => eq(alerts_payload[:data][:alerts][type.to_sym].to_s) - ) - end - - expect(body_as_json.with_indifferent_access) - .to include( - endpoint: create_payload[:subscription][:endpoint], - alerts: alerts_payload[:data][:alerts], - policy: alerts_payload[:data][:policy] - ) - end - end - - describe 'DELETE /api/v1/push/subscription' do - subject { delete '/api/v1/push/subscription', headers: headers } - - before { create_subscription_with_token } - - it 'removes the subscription' do - expect { subject } - .to change { endpoint_push_subscription }.to(nil) - end - end - - private - - def endpoint_push_subscriptions - Web::PushSubscription.where( - endpoint: create_payload[:subscription][:endpoint] - ) - end - - def endpoint_push_subscription - endpoint_push_subscriptions.first - end - - def create_subscription_with_token - Fabricate( - :web_push_subscription, - endpoint: create_payload[:subscription][:endpoint], - access_token_id: token.id - ) - end -end diff --git a/spec/requests/api/v1/reports_spec.rb b/spec/requests/api/v1/reports_spec.rb deleted file mode 100644 index a72d9bbcd8..0000000000 --- a/spec/requests/api/v1/reports_spec.rb +++ /dev/null @@ -1,94 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Reports' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'write:reports' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'POST /api/v1/reports' do - subject do - post '/api/v1/reports', headers: headers, params: params - end - - let!(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - let(:status) { Fabricate(:status) } - let(:target_account) { status.account } - let(:category) { 'other' } - let(:forward) { nil } - let(:rule_ids) { nil } - - let(:params) do - { - status_ids: [status.id], - account_id: target_account.id, - comment: 'reasons', - category: category, - rule_ids: rule_ids, - forward: forward, - } - end - - it_behaves_like 'forbidden for wrong scope', 'read read:reports' - - it 'creates a report', :aggregate_failures, :inline_jobs do - emails = capture_emails { subject } - - expect(response).to have_http_status(200) - expect(body_as_json).to match( - a_hash_including( - status_ids: [status.id.to_s], - category: category, - comment: 'reasons' - ) - ) - - expect(target_account.targeted_reports).to_not be_empty - expect(target_account.targeted_reports.first.comment).to eq 'reasons' - expect(target_account.targeted_reports.first.application).to eq token.application - - expect(emails.size) - .to eq(1) - expect(emails.first) - .to have_attributes( - to: contain_exactly(admin.email), - subject: eq(I18n.t('admin_mailer.new_report.subject', instance: Rails.configuration.x.local_domain, id: target_account.targeted_reports.first.id)) - ) - end - - context 'when a status does not belong to the reported account' do - let(:target_account) { Fabricate(:account) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when a category is chosen' do - let(:category) { 'spam' } - - it 'saves category' do - subject - - expect(target_account.targeted_reports.first.spam?).to be true - end - end - - context 'when violated rules are chosen' do - let(:rule) { Fabricate(:rule) } - let(:category) { 'violation' } - let(:rule_ids) { [rule.id] } - - it 'saves category and rule_ids' do - subject - - expect(target_account.targeted_reports.first.violation?).to be true - expect(target_account.targeted_reports.first.rule_ids).to contain_exactly(rule.id) - end - end - end -end diff --git a/spec/requests/api/v1/scheduled_status_spec.rb b/spec/requests/api/v1/scheduled_status_spec.rb deleted file mode 100644 index f4612410bf..0000000000 --- a/spec/requests/api/v1/scheduled_status_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Scheduled Statuses' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/scheduled_statuses' do - context 'when not authorized' do - it 'returns http unauthorized' do - get api_v1_scheduled_statuses_path - - expect(response) - .to have_http_status(401) - end - end - - context 'with wrong scope' do - before do - get api_v1_scheduled_statuses_path, headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'write write:statuses' - end - - context 'with an application token' do - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: 'read:statuses') } - - it 'returns http unprocessable entity' do - get api_v1_scheduled_statuses_path, headers: headers - - expect(response) - .to have_http_status(422) - end - end - - context 'with correct scope' do - let(:scopes) { 'read:statuses' } - - context 'without scheduled statuses' do - it 'returns http success without json' do - get api_v1_scheduled_statuses_path, headers: headers - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to_not be_present - end - end - - context 'with scheduled statuses' do - let!(:scheduled_status) { Fabricate(:scheduled_status, account: user.account) } - - it 'returns http success and status json' do - get api_v1_scheduled_statuses_path, headers: headers - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - .and have_attributes( - first: include(id: scheduled_status.id.to_s) - ) - end - end - end - end -end diff --git a/spec/requests/api/v1/statuses/bookmarks_spec.rb b/spec/requests/api/v1/statuses/bookmarks_spec.rb deleted file mode 100644 index d3007740a5..0000000000 --- a/spec/requests/api/v1/statuses/bookmarks_spec.rb +++ /dev/null @@ -1,155 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Bookmarks' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'write:bookmarks' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'POST /api/v1/statuses/:status_id/bookmark' do - subject do - post "/api/v1/statuses/#{status.id}/bookmark", headers: headers - end - - let(:status) { Fabricate(:status) } - - it_behaves_like 'forbidden for wrong scope', 'read' - - context 'with public status' do - it 'bookmarks the status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.bookmarked?(status)).to be true - end - - it 'returns json with updated attributes' do - subject - - expect(body_as_json).to match( - a_hash_including(id: status.id.to_s, bookmarked: true) - ) - end - end - - context 'with private status of not-followed account' do - let(:status) { Fabricate(:status, visibility: :private) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'with private status of followed account' do - let(:status) { Fabricate(:status, visibility: :private) } - - before do - user.account.follow!(status.account) - end - - it 'bookmarks the status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.bookmarked?(status)).to be true - end - end - - context 'when the status does not exist' do - it 'returns http not found' do - post '/api/v1/statuses/-1/bookmark', headers: headers - - expect(response).to have_http_status(404) - end - end - - context 'without an authorization header' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end - - describe 'POST /api/v1/statuses/:status_id/unbookmark' do - subject do - post "/api/v1/statuses/#{status.id}/unbookmark", headers: headers - end - - let(:status) { Fabricate(:status) } - - it_behaves_like 'forbidden for wrong scope', 'read' - - context 'with public status' do - context 'when the status was previously bookmarked' do - before do - Bookmark.find_or_create_by!(account: user.account, status: status) - end - - it 'unbookmarks the status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.bookmarked?(status)).to be false - end - - it 'returns json with updated attributes' do - subject - - expect(body_as_json).to match( - a_hash_including(id: status.id.to_s, bookmarked: false) - ) - end - end - - context 'when the requesting user was blocked by the status author' do - let(:status) { Fabricate(:status) } - - before do - Bookmark.find_or_create_by!(account: user.account, status: status) - status.account.block!(user.account) - end - - it 'unbookmarks the status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.bookmarked?(status)).to be false - end - - it 'returns json with updated attributes' do - subject - - expect(body_as_json).to match( - a_hash_including(id: status.id.to_s, bookmarked: false) - ) - end - end - - context 'when the status is not bookmarked' do - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - end - end - - context 'with private status that was not bookmarked' do - let(:status) { Fabricate(:status, visibility: :private) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb b/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb deleted file mode 100644 index 2fd79f424b..0000000000 --- a/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V1 Statuses Favourited by Accounts' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:accounts' } - # let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:alice) { Fabricate(:account) } - let(:bob) { Fabricate(:account) } - - context 'with an oauth token' do - subject do - get "/api/v1/statuses/#{status.id}/favourited_by", headers: headers, params: { limit: 2 } - end - - describe 'GET /api/v1/statuses/:status_id/favourited_by' do - let(:status) { Fabricate(:status, account: user.account) } - - before do - Favourite.create!(account: alice, status: status) - Favourite.create!(account: bob, status: status) - end - - it 'returns http success and accounts who favourited the status' do - subject - - expect(response) - .to have_http_status(200) - .and include_pagination_headers( - prev: api_v1_status_favourited_by_index_url(limit: 2, since_id: Favourite.last.id), - next: api_v1_status_favourited_by_index_url(limit: 2, max_id: Favourite.first.id) - ) - - expect(body_as_json.size) - .to eq(2) - expect(body_as_json) - .to contain_exactly( - include(id: alice.id.to_s), - include(id: bob.id.to_s) - ) - end - - it 'does not return blocked users' do - user.account.block!(bob) - - subject - - expect(body_as_json.size) - .to eq 1 - expect(body_as_json.first[:id]).to eq(alice.id.to_s) - end - end - end - - context 'without an oauth token' do - subject do - get "/api/v1/statuses/#{status.id}/favourited_by", params: { limit: 2 } - end - - context 'with a private status' do - let(:status) { Fabricate(:status, account: user.account, visibility: :private) } - - describe 'GET #index' do - before do - Fabricate(:favourite, status: status) - end - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(404) - end - end - end - - context 'with a public status' do - let(:status) { Fabricate(:status, account: user.account, visibility: :public) } - - describe 'GET #index' do - before do - Fabricate(:favourite, status: status) - end - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - end - end - end -end diff --git a/spec/requests/api/v1/statuses/favourites_spec.rb b/spec/requests/api/v1/statuses/favourites_spec.rb deleted file mode 100644 index 22d0e4831f..0000000000 --- a/spec/requests/api/v1/statuses/favourites_spec.rb +++ /dev/null @@ -1,145 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Favourites', :inline_jobs do - let(:user) { Fabricate(:user) } - let(:scopes) { 'write:favourites' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'POST /api/v1/statuses/:status_id/favourite' do - subject do - post "/api/v1/statuses/#{status.id}/favourite", headers: headers - end - - let(:status) { Fabricate(:status) } - - it_behaves_like 'forbidden for wrong scope', 'read read:favourites' - - context 'with public status' do - it 'favourites the status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.favourited?(status)).to be true - end - - it 'returns json with updated attributes' do - subject - - expect(body_as_json).to match( - a_hash_including(id: status.id.to_s, favourites_count: 1, favourited: true) - ) - end - end - - context 'with private status of not-followed account' do - let(:status) { Fabricate(:status, visibility: :private) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'with private status of followed account' do - let(:status) { Fabricate(:status, visibility: :private) } - - before do - user.account.follow!(status.account) - end - - it 'favourites the status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.favourited?(status)).to be true - end - end - - context 'without an authorization header' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end - - describe 'POST /api/v1/statuses/:status_id/unfavourite' do - subject do - post "/api/v1/statuses/#{status.id}/unfavourite", headers: headers - end - - let(:status) { Fabricate(:status) } - - it_behaves_like 'forbidden for wrong scope', 'read read:favourites' - - context 'with public status' do - before do - FavouriteService.new.call(user.account, status) - end - - it 'unfavourites the status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - - expect(user.account.favourited?(status)).to be false - end - - it 'returns json with updated attributes' do - subject - - expect(body_as_json).to match( - a_hash_including(id: status.id.to_s, favourites_count: 0, favourited: false) - ) - end - end - - context 'when the requesting user was blocked by the status author' do - before do - FavouriteService.new.call(user.account, status) - status.account.block!(user.account) - end - - it 'unfavourites the status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - - expect(user.account.favourited?(status)).to be false - end - - it 'returns json with updated attributes' do - subject - - expect(body_as_json).to match( - a_hash_including(id: status.id.to_s, favourites_count: 0, favourited: false) - ) - end - end - - context 'when status is not favourited' do - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - end - - context 'with private status that was not favourited' do - let(:status) { Fabricate(:status, visibility: :private) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v1/statuses/histories_spec.rb b/spec/requests/api/v1/statuses/histories_spec.rb deleted file mode 100644 index b3761ca688..0000000000 --- a/spec/requests/api/v1/statuses/histories_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Statuses Histories' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:statuses' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - context 'with an oauth token' do - describe 'GET /api/v1/statuses/:status_id/history' do - let(:status) { Fabricate(:status, account: user.account) } - - before do - get "/api/v1/statuses/#{status.id}/history", headers: headers - end - - it 'returns http success' do - expect(response).to have_http_status(200) - expect(body_as_json.size).to_not be 0 - end - end - end -end diff --git a/spec/requests/api/v1/statuses/mutes_spec.rb b/spec/requests/api/v1/statuses/mutes_spec.rb deleted file mode 100644 index 72fd7d9d11..0000000000 --- a/spec/requests/api/v1/statuses/mutes_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Statuses Mutes' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'write:mutes' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - context 'with an oauth token' do - describe 'POST /api/v1/statuses/:status_id/mute' do - let(:status) { Fabricate(:status, account: user.account) } - - before do - post "/api/v1/statuses/#{status.id}/mute", headers: headers - end - - it 'creates a conversation mute', :aggregate_failures do - expect(response).to have_http_status(200) - expect(ConversationMute.find_by(account: user.account, conversation_id: status.conversation_id)).to_not be_nil - end - end - - describe 'POST /api/v1/statuses/:status_id/unmute' do - let(:status) { Fabricate(:status, account: user.account) } - - before do - user.account.mute_conversation!(status.conversation) - post "/api/v1/statuses/#{status.id}/unmute", headers: headers - end - - it 'destroys the conversation mute', :aggregate_failures do - expect(response).to have_http_status(200) - expect(ConversationMute.find_by(account: user.account, conversation_id: status.conversation_id)).to be_nil - end - end - end -end diff --git a/spec/requests/api/v1/statuses/pins_spec.rb b/spec/requests/api/v1/statuses/pins_spec.rb deleted file mode 100644 index db07fa424f..0000000000 --- a/spec/requests/api/v1/statuses/pins_spec.rb +++ /dev/null @@ -1,131 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Pins' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'write:accounts' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'POST /api/v1/statuses/:status_id/pin' do - subject do - post "/api/v1/statuses/#{status.id}/pin", headers: headers - end - - let(:status) { Fabricate(:status, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'read read:accounts' - - context 'when the status is public' do - it 'pins the status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.pinned?(status)).to be true - end - - it 'return json with updated attributes' do - subject - - expect(body_as_json).to match( - a_hash_including(id: status.id.to_s, pinned: true) - ) - end - end - - context 'when the status is private' do - let(:status) { Fabricate(:status, account: user.account, visibility: :private) } - - it 'pins the status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.pinned?(status)).to be true - end - end - - context 'when the status belongs to somebody else' do - let(:status) { Fabricate(:status) } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the status does not exist' do - it 'returns http not found' do - post '/api/v1/statuses/-1/pin', headers: headers - - expect(response).to have_http_status(404) - end - end - - context 'without an authorization header' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end - - describe 'POST /api/v1/statuses/:status_id/unpin' do - subject do - post "/api/v1/statuses/#{status.id}/unpin", headers: headers - end - - let(:status) { Fabricate(:status, account: user.account) } - - context 'when the status is pinned' do - before do - Fabricate(:status_pin, status: status, account: user.account) - end - - it 'unpins the status successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(user.account.pinned?(status)).to be false - end - - it 'return json with updated attributes' do - subject - - expect(body_as_json).to match( - a_hash_including(id: status.id.to_s, pinned: false) - ) - end - end - - context 'when the status is not pinned' do - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - end - - context 'when the status does not exist' do - it 'returns http not found' do - post '/api/v1/statuses/-1/unpin', headers: headers - - expect(response).to have_http_status(404) - end - end - - context 'without an authorization header' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb b/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb deleted file mode 100644 index 5fc54042f9..0000000000 --- a/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb +++ /dev/null @@ -1,94 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V1 Statuses Reblogged by Accounts' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:accounts' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:alice) { Fabricate(:account) } - let(:bob) { Fabricate(:account) } - - context 'with an oauth token' do - subject do - get "/api/v1/statuses/#{status.id}/reblogged_by", headers: headers, params: { limit: 2 } - end - - describe 'GET /api/v1/statuses/:status_id/reblogged_by' do - let(:status) { Fabricate(:status, account: user.account) } - - before do - Fabricate(:status, account: alice, reblog_of_id: status.id) - Fabricate(:status, account: bob, reblog_of_id: status.id) - end - - it 'returns accounts who reblogged the status', :aggregate_failures do - subject - - expect(response) - .to have_http_status(200) - .and include_pagination_headers( - prev: api_v1_status_reblogged_by_index_url(limit: 2, since_id: bob.statuses.first.id), - next: api_v1_status_reblogged_by_index_url(limit: 2, max_id: alice.statuses.first.id) - ) - - expect(body_as_json.size) - .to eq(2) - expect(body_as_json) - .to contain_exactly( - include(id: alice.id.to_s), - include(id: bob.id.to_s) - ) - end - - it 'does not return blocked users' do - user.account.block!(bob) - - subject - - expect(body_as_json.size) - .to eq 1 - expect(body_as_json.first[:id]).to eq(alice.id.to_s) - end - end - end - - context 'without an oauth token' do - subject do - get "/api/v1/statuses/#{status.id}/reblogged_by", params: { limit: 2 } - end - - context 'with a private status' do - let(:status) { Fabricate(:status, account: user.account, visibility: :private) } - - describe 'GET #index' do - before do - Fabricate(:status, reblog_of_id: status.id) - end - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(404) - end - end - end - - context 'with a public status' do - let(:status) { Fabricate(:status, account: user.account, visibility: :public) } - - describe 'GET #index' do - before do - Fabricate(:status, reblog_of_id: status.id) - end - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - end - end - end -end diff --git a/spec/requests/api/v1/statuses/reblogs_spec.rb b/spec/requests/api/v1/statuses/reblogs_spec.rb deleted file mode 100644 index 503d804ed0..0000000000 --- a/spec/requests/api/v1/statuses/reblogs_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Statuses Reblogs' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'write:statuses' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - context 'with an oauth token' do - describe 'POST /api/v1/statuses/:status_id/reblog' do - let(:status) { Fabricate(:status, account: user.account) } - - before do - post "/api/v1/statuses/#{status.id}/reblog", headers: headers - end - - context 'with public status' do - it 'reblogs the status', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(status.reblogs.count).to eq 1 - - expect(user.account.reblogged?(status)).to be true - - hash_body = body_as_json - - expect(hash_body[:reblog][:id]).to eq status.id.to_s - expect(hash_body[:reblog][:reblogs_count]).to eq 1 - expect(hash_body[:reblog][:reblogged]).to be true - end - end - - context 'with private status of not-followed account' do - let(:status) { Fabricate(:status, visibility: :private) } - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/statuses/:status_id/unreblog', :inline_jobs do - context 'with public status' do - let(:status) { Fabricate(:status, account: user.account) } - - before do - ReblogService.new.call(user.account, status) - post "/api/v1/statuses/#{status.id}/unreblog", headers: headers - end - - it 'destroys the reblog', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(status.reblogs.count).to eq 0 - - expect(user.account.reblogged?(status)).to be false - - hash_body = body_as_json - - expect(hash_body[:id]).to eq status.id.to_s - expect(hash_body[:reblogs_count]).to eq 0 - expect(hash_body[:reblogged]).to be false - end - end - - context 'with public status when blocked by its author' do - let(:status) { Fabricate(:status, account: user.account) } - - before do - ReblogService.new.call(user.account, status) - status.account.block!(user.account) - post "/api/v1/statuses/#{status.id}/unreblog", headers: headers - end - - it 'destroys the reblog', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(status.reblogs.count).to eq 0 - - expect(user.account.reblogged?(status)).to be false - - hash_body = body_as_json - - expect(hash_body[:id]).to eq status.id.to_s - expect(hash_body[:reblogs_count]).to eq 0 - expect(hash_body[:reblogged]).to be false - end - end - - context 'with private status that was not reblogged' do - let(:status) { Fabricate(:status, visibility: :private) } - - before do - post "/api/v1/statuses/#{status.id}/unreblog", headers: headers - end - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end - end -end diff --git a/spec/requests/api/v1/statuses/sources_spec.rb b/spec/requests/api/v1/statuses/sources_spec.rb deleted file mode 100644 index c79ec89648..0000000000 --- a/spec/requests/api/v1/statuses/sources_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Sources' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:statuses' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/statuses/:status_id/source' do - subject do - get "/api/v1/statuses/#{status.id}/source", headers: headers - end - - let(:status) { Fabricate(:status) } - - it_behaves_like 'forbidden for wrong scope', 'write write:statuses' - - context 'with public status' do - it 'returns the source properties of the status', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to eq({ - id: status.id.to_s, - text: status.text, - spoiler_text: status.spoiler_text, - content_type: nil, - }) - end - end - - context 'with private status of non-followed account' do - let(:status) { Fabricate(:status, visibility: :private) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'with private status of followed account' do - let(:status) { Fabricate(:status, visibility: :private) } - - before do - user.account.follow!(status.account) - end - - it 'returns the source properties of the status', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json).to eq({ - id: status.id.to_s, - text: status.text, - spoiler_text: status.spoiler_text, - content_type: nil, - }) - end - end - - context 'without an authorization header' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v1/statuses/translations_spec.rb b/spec/requests/api/v1/statuses/translations_spec.rb deleted file mode 100644 index e2ab5d0b80..0000000000 --- a/spec/requests/api/v1/statuses/translations_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Statuses Translations' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:statuses' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - context 'with an application token' do - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) } - - describe 'POST /api/v1/statuses/:status_id/translate' do - let(:status) { Fabricate(:status, account: user.account, text: 'Hola', language: 'es') } - - before do - post "/api/v1/statuses/#{status.id}/translate", headers: headers - end - - it 'returns http unprocessable entity' do - expect(response).to have_http_status(422) - end - end - end - - context 'with an oauth token' do - describe 'POST /api/v1/statuses/:status_id/translate' 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]) - allow(TranslationService).to receive_messages(configured?: true, configured: service) - Rails.cache.write('translation_service/languages', { 'es' => ['en'] }) - post "/api/v1/statuses/#{status.id}/translate", headers: headers - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - end - end -end diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb deleted file mode 100644 index 2f99b35e74..0000000000 --- a/spec/requests/api/v1/statuses_spec.rb +++ /dev/null @@ -1,310 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe '/api/v1/statuses' do - context 'with an oauth token' do - let(:user) { Fabricate(:user) } - let(:client_app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: client_app, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/statuses?id[]=:id' do - let(:status) { Fabricate(:status) } - let(:other_status) { Fabricate(:status) } - let(:scopes) { 'read:statuses' } - - it 'returns expected response' do - get '/api/v1/statuses', headers: headers, params: { id: [status.id, other_status.id, 123_123] } - - expect(response).to have_http_status(200) - expect(body_as_json).to contain_exactly( - hash_including(id: status.id.to_s), - hash_including(id: other_status.id.to_s) - ) - end - end - - describe 'GET /api/v1/statuses/:id' do - subject do - get "/api/v1/statuses/#{status.id}", headers: headers - end - - let(:scopes) { 'read:statuses' } - let(:status) { Fabricate(:status, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'write write:statuses' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - context 'when post includes filtered terms' do - let(:status) { Fabricate(:status, text: 'this toot is about that banned word') } - - before do - user.account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }]) - end - - it 'returns filter information', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:filtered][0]).to include({ - filter: a_hash_including({ - id: user.account.custom_filters.first.id.to_s, - title: 'filter1', - filter_action: 'hide', - }), - keyword_matches: ['banned'], - }) - end - end - - context 'when post is explicitly filtered' do - let(:status) { Fabricate(:status, text: 'hello world') } - - before do - filter = user.account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide) - filter.statuses.create!(status_id: status.id) - end - - it 'returns filter information', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:filtered][0]).to include({ - filter: a_hash_including({ - id: user.account.custom_filters.first.id.to_s, - title: 'filter1', - filter_action: 'hide', - }), - status_matches: [status.id.to_s], - }) - end - end - - context 'when reblog includes filtered terms' do - let(:status) { Fabricate(:status, reblog: Fabricate(:status, text: 'this toot is about that banned word')) } - - before do - user.account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }]) - end - - it 'returns filter information', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:reblog][:filtered][0]).to include({ - filter: a_hash_including({ - id: user.account.custom_filters.first.id.to_s, - title: 'filter1', - filter_action: 'hide', - }), - keyword_matches: ['banned'], - }) - end - end - end - - describe 'GET /api/v1/statuses/:id/context' do - let(:scopes) { 'read:statuses' } - let(:status) { Fabricate(:status, account: user.account) } - - before do - Fabricate(:status, account: user.account, thread: status) - end - - it 'returns http success' do - get "/api/v1/statuses/#{status.id}/context", headers: headers - - expect(response).to have_http_status(200) - end - end - - describe 'POST /api/v1/statuses' do - subject do - post '/api/v1/statuses', headers: headers, params: params - end - - let(:scopes) { 'write:statuses' } - let(:params) { { status: 'Hello world' } } - - it_behaves_like 'forbidden for wrong scope', 'read read:statuses' - - context 'with a basic status body' do - it 'returns rate limit headers', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s - expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s - end - end - - context 'with a safeguard' do - let!(:alice) { Fabricate(:account, username: 'alice') } - let!(:bob) { Fabricate(:account, username: 'bob') } - - let(:params) { { status: '@alice hm, @bob is really annoying lately', allowed_mentions: [alice.id] } } - - it 'returns serialized extra accounts in body', :aggregate_failures do - subject - - expect(response).to have_http_status(422) - expect(body_as_json[:unexpected_accounts].map { |a| a.slice(:id, :acct) }).to eq [{ id: bob.id.to_s, acct: bob.acct }] - end - end - - context 'with missing parameters' do - let(:params) { {} } - - it 'returns rate limit headers', :aggregate_failures do - subject - - expect(response).to have_http_status(422) - expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s - end - end - - context 'when exceeding rate limit' do - before do - rate_limiter = RateLimiter.new(user.account, family: :statuses) - RateLimiter::FAMILIES[:statuses][:limit].times { rate_limiter.record! } - end - - it 'returns rate limit headers', :aggregate_failures do - subject - - expect(response).to have_http_status(429) - expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s - expect(response.headers['X-RateLimit-Remaining']).to eq '0' - end - end - - context 'with missing thread' do - let(:params) { { status: 'Hello world', in_reply_to_id: 0 } } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when scheduling a status' do - let(:params) { { status: 'Hello world', scheduled_at: 10.minutes.from_now } } - let(:account) { user.account } - - it 'returns HTTP 200' do - subject - - expect(response).to have_http_status(200) - end - - it 'creates a scheduled status' do - expect { subject }.to change { account.scheduled_statuses.count }.from(0).to(1) - end - - context 'when the scheduling time is less than 5 minutes' do - let(:params) { { status: 'Hello world', scheduled_at: 4.minutes.from_now } } - - it 'does not create a scheduled status', :aggregate_failures do - subject - - expect(response).to have_http_status(422) - expect(account.scheduled_statuses).to be_empty - end - end - end - end - - describe 'DELETE /api/v1/statuses/:id' do - subject do - delete "/api/v1/statuses/#{status.id}", headers: headers - end - - let(:scopes) { 'write:statuses' } - let(:status) { Fabricate(:status, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'read read:statuses' - - it 'removes the status', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(Status.find_by(id: status.id)).to be_nil - end - end - - describe 'PUT /api/v1/statuses/:id' do - subject do - put "/api/v1/statuses/#{status.id}", headers: headers, params: { status: 'I am updated' } - end - - let(:scopes) { 'write:statuses' } - let(:status) { Fabricate(:status, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'read read:statuses' - - it 'updates the status', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(status.reload.text).to eq 'I am updated' - end - end - end - - context 'without an oauth token' do - context 'with a private status' do - let(:status) { Fabricate(:status, visibility: :private) } - - describe 'GET /api/v1/statuses/:id' do - it 'returns http unauthorized' do - get "/api/v1/statuses/#{status.id}" - - expect(response).to have_http_status(404) - end - end - - describe 'GET /api/v1/statuses/:id/context' do - before do - Fabricate(:status, thread: status) - end - - it 'returns http unauthorized' do - get "/api/v1/statuses/#{status.id}/context" - - expect(response).to have_http_status(404) - end - end - end - - context 'with a public status' do - let(:status) { Fabricate(:status, visibility: :public) } - - describe 'GET /api/v1/statuses/:id' do - it 'returns http success' do - get "/api/v1/statuses/#{status.id}" - - expect(response).to have_http_status(200) - end - end - - describe 'GET /api/v1/statuses/:id/context' do - before do - Fabricate(:status, thread: status) - end - - it 'returns http success' do - get "/api/v1/statuses/#{status.id}/context" - - expect(response).to have_http_status(200) - end - end - end - end -end diff --git a/spec/requests/api/v1/streaming_spec.rb b/spec/requests/api/v1/streaming_spec.rb deleted file mode 100644 index 6b550dfa60..0000000000 --- a/spec/requests/api/v1/streaming_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Streaming' do - around do |example| - before = Rails.configuration.x.streaming_api_base_url - Rails.configuration.x.streaming_api_base_url = "wss://#{Rails.configuration.x.web_domain}" - example.run - Rails.configuration.x.streaming_api_base_url = before - end - - let(:headers) { { 'Host' => Rails.configuration.x.web_domain } } - - context 'with streaming api on same host' do - describe 'GET /api/v1/streaming' do - it 'raises ActiveRecord::RecordNotFound' do - get '/api/v1/streaming', headers: headers - - expect(response).to have_http_status(404) - end - end - end - - context 'with streaming api on different host' do - before do - Rails.configuration.x.streaming_api_base_url = "wss://streaming-#{Rails.configuration.x.web_domain}" - end - - describe 'GET /api/v1/streaming' do - it 'redirects to streaming host' do - get '/api/v1/streaming', headers: headers, params: { access_token: 'deadbeef', stream: 'public' } - - expect(response) - .to have_http_status(301) - - expect(redirect_to_uri) - .to have_attributes( - fragment: request_uri.fragment, - host: eq(streaming_host), - path: request_uri.path, - query: request_uri.query, - scheme: request_uri.scheme - ) - end - - private - - def request_uri - URI.parse(request.url) - end - - def redirect_to_uri - URI.parse(response.location) - end - - def streaming_host - URI.parse(Rails.configuration.x.streaming_api_base_url).host - end - end - end -end diff --git a/spec/requests/api/v1/suggestions_spec.rb b/spec/requests/api/v1/suggestions_spec.rb deleted file mode 100644 index dc89613fc5..0000000000 --- a/spec/requests/api/v1/suggestions_spec.rb +++ /dev/null @@ -1,99 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Suggestions' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/suggestions' do - subject do - get '/api/v1/suggestions', headers: headers, params: params - end - - let(:bob) { Fabricate(:account) } - let(:jeff) { Fabricate(:account) } - let(:params) { {} } - - before do - Setting.bootstrap_timeline_accounts = [bob, jeff].map(&:acct).join(',') - end - - it_behaves_like 'forbidden for wrong scope', 'write' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'returns accounts' do - subject - - body = body_as_json - - expect(body.size).to eq 2 - expect(body.pluck(:id)).to match_array([bob, jeff].map { |i| i.id.to_s }) - end - - context 'with limit param' do - let(:params) { { limit: 1 } } - - it 'returns only the requested number of accounts' do - subject - - expect(body_as_json.size).to eq 1 - end - end - - context 'without an authorization header' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end - - describe 'DELETE /api/v1/suggestions/:id' do - subject do - delete "/api/v1/suggestions/#{jeff.id}", headers: headers - end - - let(:bob) { Fabricate(:account) } - let(:jeff) { Fabricate(:account) } - let(:scopes) { 'write' } - - before do - Setting.bootstrap_timeline_accounts = [bob, jeff].map(&:acct).join(',') - end - - it_behaves_like 'forbidden for wrong scope', 'read' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'removes the specified suggestion' do - subject - - expect(FollowRecommendationMute.exists?(account: user.account, target_account: jeff)).to be true - end - - context 'without an authorization header' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v1/tags_spec.rb b/spec/requests/api/v1/tags_spec.rb deleted file mode 100644 index db74a6f037..0000000000 --- a/spec/requests/api/v1/tags_spec.rb +++ /dev/null @@ -1,144 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Tags' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'write:follows' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/tags/:id' do - subject do - get "/api/v1/tags/#{name}" - end - - context 'when the tag exists' do - let!(:tag) { Fabricate(:tag) } - let(:name) { tag.name } - - it 'returns the tag', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:name]).to eq(name) - end - end - - context 'when the tag does not exist' do - let(:name) { 'hoge' } - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - end - - context 'when the tag name is invalid' do - let(:name) { 'tag-name' } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v1/tags/:id/follow' do - subject do - post "/api/v1/tags/#{name}/follow", headers: headers - end - - let!(:tag) { Fabricate(:tag) } - let(:name) { tag.name } - - it_behaves_like 'forbidden for wrong scope', 'read read:follows' - - context 'when the tag exists' do - it 'creates follow', :aggregate_failures do - subject - - expect(response).to have_http_status(:success) - expect(TagFollow.where(tag: tag, account: user.account)).to exist - end - end - - context 'when the tag does not exist' do - let(:name) { 'hoge' } - - it 'creates a new tag with the specified name', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(Tag.where(name: name)).to exist - expect(TagFollow.where(tag: Tag.find_by(name: name), account: user.account)).to exist - end - end - - context 'when the tag name is invalid' do - let(:name) { 'tag-name' } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when the Authorization header is missing' do - let(:headers) { {} } - let(:name) { 'unauthorized' } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end - - describe 'POST #unfollow' do - subject do - post "/api/v1/tags/#{name}/unfollow", headers: headers - end - - let(:name) { tag.name } - let!(:tag) { Fabricate(:tag, name: 'foo') } - - before do - Fabricate(:tag_follow, account: user.account, tag: tag) - end - - it_behaves_like 'forbidden for wrong scope', 'read read:follows' - - it 'removes the follow', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(TagFollow.where(tag: tag, account: user.account)).to_not exist - end - - context 'when the tag name is invalid' do - let(:name) { 'tag-name' } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when the Authorization header is missing' do - let(:headers) { {} } - let(:name) { 'unauthorized' } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v1/timelines/direct_spec.rb b/spec/requests/api/v1/timelines/direct_spec.rb deleted file mode 100644 index f882e4ccc5..0000000000 --- a/spec/requests/api/v1/timelines/direct_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Direct Timeline' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:statuses' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/timelines/direct' do - it 'returns 200' do - get '/api/v1/timelines/direct', headers: headers - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/v1/timelines/home_spec.rb b/spec/requests/api/v1/timelines/home_spec.rb deleted file mode 100644 index 96bd153aff..0000000000 --- a/spec/requests/api/v1/timelines/home_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Home', :inline_jobs do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:statuses' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v1/timelines/home' do - subject do - get '/api/v1/timelines/home', headers: headers, params: params - end - - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'write write:statuses' - - context 'when the timeline is available' do - let(:home_statuses) { bob.statuses + ana.statuses } - let!(:bob) { Fabricate(:account) } - let!(:tim) { Fabricate(:account) } - let!(:ana) { Fabricate(:account) } - - before do - user.account.follow!(bob) - user.account.follow!(ana) - PostStatusService.new.call(bob, text: 'New toot from bob.') - PostStatusService.new.call(tim, text: 'New toot from tim.') - PostStatusService.new.call(ana, text: 'New toot from ana.') - end - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'returns the statuses of followed users' do - subject - - expect(body_as_json.pluck(:id)).to match_array(home_statuses.map { |status| status.id.to_s }) - end - - context 'with limit param' do - let(:params) { { limit: 1 } } - - it 'returns only the requested number of statuses' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - - it 'sets the correct pagination headers', :aggregate_failures do - subject - - expect(response) - .to include_pagination_headers( - prev: api_v1_timelines_home_url(limit: params[:limit], min_id: ana.statuses.first.id), - next: api_v1_timelines_home_url(limit: params[:limit], max_id: ana.statuses.first.id) - ) - end - end - end - - context 'when the timeline is regenerating' do - let(:timeline) { instance_double(HomeFeed, regenerating?: true, get: []) } - - before do - allow(HomeFeed).to receive(:new).and_return(timeline) - end - - it 'returns http partial content' do - subject - - expect(response).to have_http_status(206) - end - end - - context 'without an authorization header' do - let(:headers) { {} } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - - context 'without a user context' do - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) } - - it 'returns http unprocessable entity', :aggregate_failures do - subject - - expect(response).to have_http_status(422) - expect(response.headers['Link']).to be_nil - end - end - end -end diff --git a/spec/requests/api/v1/timelines/link_spec.rb b/spec/requests/api/v1/timelines/link_spec.rb deleted file mode 100644 index 57969fbd0e..0000000000 --- a/spec/requests/api/v1/timelines/link_spec.rb +++ /dev/null @@ -1,149 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Link' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:statuses' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - shared_examples 'a successful request to the link timeline' do - it 'returns the expected statuses successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.pluck(:id)).to match_array(expected_statuses.map { |status| status.id.to_s }) - end - end - - describe 'GET /api/v1/timelines/link' do - subject do - get '/api/v1/timelines/link', headers: headers, params: params - end - - let(:url) { 'https://example.com/' } - let(:private_status) { Fabricate(:status, visibility: :private) } - let(:undiscoverable_status) { Fabricate(:status, account: Fabricate.build(:account, domain: nil, discoverable: false)) } - let(:local_status) { Fabricate(:status, account: Fabricate.build(:account, domain: nil, discoverable: true)) } - let(:remote_status) { Fabricate(:status, account: Fabricate.build(:account, domain: 'example.com', discoverable: true)) } - let(:params) { { url: url } } - let(:expected_statuses) { [local_status, remote_status] } - let(:preview_card) { Fabricate(:preview_card, url: url) } - - before do - if preview_card.present? - preview_card.create_trend!(allowed: true) - - [private_status, undiscoverable_status, remote_status, local_status].each do |status| - PreviewCardsStatus.create(status: status, preview_card: preview_card, url: url) - end - end - end - - it_behaves_like 'forbidden for wrong scope', 'profile' - - context 'when there is no preview card' do - let(:preview_card) { nil } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when preview card is not trending' do - before do - preview_card.trend.destroy! - end - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when preview card is trending but not approved' do - before do - preview_card.trend.update(allowed: false) - end - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when the instance does not allow public preview' do - before do - Form::AdminSettings.new(timeline_preview: false).save - end - - it_behaves_like 'forbidden for wrong scope', 'profile' - - context 'without an authentication token' do - let(:headers) { {} } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'with an application access token, not bound to a user' do - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the user is authenticated' do - it_behaves_like 'a successful request to the link timeline' - end - end - - context 'when the instance allows public preview' do - before do - Setting.timeline_preview = true - end - - context 'with an authorized user' do - it_behaves_like 'a successful request to the link timeline' - end - - context 'with an anonymous user' do - let(:headers) { {} } - - it_behaves_like 'a successful request to the link timeline' - end - - context 'with limit param' do - let(:params) { { limit: 1, url: url } } - - it 'returns only the requested number of statuses', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq(params[:limit]) - end - - it 'sets the correct pagination headers', :aggregate_failures do - subject - - expect(response) - .to include_pagination_headers( - prev: api_v1_timelines_link_url(limit: params[:limit], url: url, min_id: local_status.id), - next: api_v1_timelines_link_url(limit: params[:limit], url: url, max_id: local_status.id) - ) - end - end - end - end -end diff --git a/spec/requests/api/v1/timelines/list_spec.rb b/spec/requests/api/v1/timelines/list_spec.rb deleted file mode 100644 index 98d2456745..0000000000 --- a/spec/requests/api/v1/timelines/list_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API V1 Timelines List' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:statuses' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - let(:list) { Fabricate(:list, account: user.account) } - - context 'with a user context' do - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:lists') } - - describe 'GET /api/v1/timelines/list/:id' do - before do - follow = Fabricate(:follow, account: user.account) - list.accounts << follow.target_account - PostStatusService.new.call(follow.target_account, text: 'New status for user home timeline.') - end - - it 'returns http success' do - get "/api/v1/timelines/list/#{list.id}", headers: headers - - expect(response).to have_http_status(200) - end - end - end - - context 'with the wrong user context' do - let(:other_user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: other_user.id, scopes: 'read') } - - describe 'GET #show' do - it 'returns http not found' do - get "/api/v1/timelines/list/#{list.id}", headers: headers - - expect(response).to have_http_status(404) - end - end - end - - context 'without a user context' do - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: 'read') } - - describe 'GET #show' do - it 'returns http unprocessable entity' do - get "/api/v1/timelines/list/#{list.id}", headers: headers - - expect(response).to have_http_status(422) - expect(response.headers['Link']).to be_nil - end - end - end -end diff --git a/spec/requests/api/v1/timelines/public_spec.rb b/spec/requests/api/v1/timelines/public_spec.rb deleted file mode 100644 index f17311af9b..0000000000 --- a/spec/requests/api/v1/timelines/public_spec.rb +++ /dev/null @@ -1,137 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Public' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:statuses' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - shared_examples 'a successful request to the public timeline' do - it 'returns the expected statuses successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.pluck(:id)).to match_array(expected_statuses.map { |status| status.id.to_s }) - end - end - - describe 'GET /api/v1/timelines/public' do - subject do - get '/api/v1/timelines/public', headers: headers, params: params - end - - let!(:local_status) { Fabricate(:status, account: Fabricate.build(:account, domain: nil)) } - let!(:remote_status) { Fabricate(:status, account: Fabricate.build(:account, domain: 'example.com')) } - let!(:media_status) { Fabricate(:status, media_attachments: [Fabricate.build(:media_attachment)]) } - let(:params) { {} } - - before do - Fabricate(:status, visibility: :private) - end - - context 'when the instance allows public preview' do - let(:expected_statuses) { [local_status, remote_status, media_status] } - - before do - Setting.timeline_preview = true - end - - it_behaves_like 'forbidden for wrong scope', 'profile' - - context 'with an authorized user' do - it_behaves_like 'a successful request to the public timeline' - end - - context 'with an anonymous user' do - let(:headers) { {} } - - it_behaves_like 'a successful request to the public timeline' - end - - context 'with local param' do - let(:params) { { local: true } } - let(:expected_statuses) { [local_status, media_status] } - - it_behaves_like 'a successful request to the public timeline' - end - - context 'with remote param' do - let(:params) { { remote: true } } - let(:expected_statuses) { [remote_status] } - - it_behaves_like 'a successful request to the public timeline' - end - - context 'with local and remote params' do - let(:params) { { local: true, remote: true } } - let(:expected_statuses) { [local_status, remote_status, media_status] } - - it_behaves_like 'a successful request to the public timeline' - end - - context 'with only_media param' do - let(:params) { { only_media: true } } - let(:expected_statuses) { [media_status] } - - it_behaves_like 'a successful request to the public timeline' - end - - context 'with limit param' do - let(:params) { { limit: 1 } } - - it 'returns only the requested number of statuses', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.size).to eq(params[:limit]) - end - - it 'sets the correct pagination headers', :aggregate_failures do - subject - - expect(response) - .to include_pagination_headers( - prev: api_v1_timelines_public_url(limit: params[:limit], min_id: media_status.id), - next: api_v1_timelines_public_url(limit: params[:limit], max_id: media_status.id) - ) - end - end - end - - context 'when the instance does not allow public preview' do - before do - Form::AdminSettings.new(timeline_preview: false).save - end - - it_behaves_like 'forbidden for wrong scope', 'profile' - - context 'without an authentication token' do - let(:headers) { {} } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'with an application access token, not bound to a user' do - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'with an authenticated user' do - let(:expected_statuses) { [local_status, remote_status, media_status] } - - it_behaves_like 'a successful request to the public timeline' - end - end - end -end diff --git a/spec/requests/api/v1/timelines/tag_spec.rb b/spec/requests/api/v1/timelines/tag_spec.rb deleted file mode 100644 index 4f2f6e5a18..0000000000 --- a/spec/requests/api/v1/timelines/tag_spec.rb +++ /dev/null @@ -1,121 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Tag' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:statuses' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - shared_examples 'a successful request to the tag timeline' do - it 'returns the expected statuses', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.pluck(:id)).to match_array(expected_statuses.map { |status| status.id.to_s }) - end - end - - describe 'GET /api/v1/timelines/tag/:hashtag' do - subject do - get "/api/v1/timelines/tag/#{hashtag}", headers: headers, params: params - end - - before do - Setting.timeline_preview = true - end - - let(:account) { Fabricate(:account) } - let!(:private_status) { PostStatusService.new.call(account, visibility: :private, text: '#life could be a dream') } # rubocop:disable RSpec/LetSetup - let!(:life_status) { PostStatusService.new.call(account, text: 'tell me what is my #life without your #love') } - let!(:war_status) { PostStatusService.new.call(user.account, text: '#war, war never changes') } - let!(:love_status) { PostStatusService.new.call(account, text: 'what is #love?') } - let(:params) { {} } - let(:hashtag) { 'life' } - - it_behaves_like 'forbidden for wrong scope', 'profile' - - context 'when given only one hashtag' do - let(:expected_statuses) { [life_status] } - - it_behaves_like 'a successful request to the tag timeline' - end - - context 'with any param' do - let(:expected_statuses) { [life_status, love_status] } - let(:params) { { any: %(love) } } - - it_behaves_like 'a successful request to the tag timeline' - end - - context 'with all param' do - let(:expected_statuses) { [life_status] } - let(:params) { { all: %w(love) } } - - it_behaves_like 'a successful request to the tag timeline' - end - - context 'with none param' do - let(:expected_statuses) { [war_status] } - let(:hashtag) { 'war' } - let(:params) { { none: %w(life love) } } - - it_behaves_like 'a successful request to the tag timeline' - end - - context 'with limit param' do - let(:hashtag) { 'love' } - let(:params) { { limit: 1 } } - - it 'returns only the requested number of statuses' do - subject - - expect(body_as_json.size).to eq(params[:limit]) - end - - it 'sets the correct pagination headers', :aggregate_failures do - subject - - expect(response) - .to include_pagination_headers( - prev: api_v1_timelines_tag_url(limit: params[:limit], min_id: love_status.id), - next: api_v1_timelines_tag_url(limit: params[:limit], max_id: love_status.id) - ) - end - end - - context 'when the instance allows public preview' do - context 'when the user is not authenticated' do - let(:headers) { {} } - let(:expected_statuses) { [life_status] } - - it_behaves_like 'a successful request to the tag timeline' - end - end - - context 'when the instance does not allow public preview' do - before do - Form::AdminSettings.new(timeline_preview: false).save - end - - it_behaves_like 'forbidden for wrong scope', 'profile' - - context 'without an authentication token' do - let(:headers) { {} } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the user is authenticated' do - let(:expected_statuses) { [life_status] } - - it_behaves_like 'a successful request to the tag timeline' - end - end - end -end diff --git a/spec/requests/api/v1/trends/links_spec.rb b/spec/requests/api/v1/trends/links_spec.rb deleted file mode 100644 index 012d035907..0000000000 --- a/spec/requests/api/v1/trends/links_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V1 Trends Links' do - describe 'GET /api/v1/trends/links' do - context 'when trends are disabled' do - before { Setting.trends = false } - - it 'returns http success' do - get '/api/v1/trends/links' - - expect(response).to have_http_status(200) - end - end - - context 'when trends are enabled' do - before { Setting.trends = true } - - it 'returns http success' do - prepare_trends - stub_const('Api::V1::Trends::LinksController::DEFAULT_LINKS_LIMIT', 2) - get '/api/v1/trends/links' - - expect(response).to have_http_status(200) - expect(response.headers).to include('Link') - end - - def prepare_trends - Fabricate.times(3, :preview_card, trendable: true, language: 'en').each do |link| - 2.times { |i| Trends.links.add(link, i) } - end - Trends::Links.new(threshold: 1).refresh - end - end - end -end diff --git a/spec/requests/api/v1/trends/statuses_spec.rb b/spec/requests/api/v1/trends/statuses_spec.rb deleted file mode 100644 index 3b906e8f82..0000000000 --- a/spec/requests/api/v1/trends/statuses_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V1 Trends Statuses' do - describe 'GET /api/v1/trends/statuses' do - context 'when trends are disabled' do - before { Setting.trends = false } - - it 'returns http success' do - get '/api/v1/trends/statuses' - - expect(response).to have_http_status(200) - end - end - - context 'when trends are enabled' do - before { Setting.trends = true } - - it 'returns http success' do - prepare_trends - stub_const('Api::BaseController::DEFAULT_STATUSES_LIMIT', 2) - get '/api/v1/trends/statuses' - - expect(response).to have_http_status(200) - expect(response.headers).to include('Link') - end - - def prepare_trends - Fabricate.times(3, :status, trendable: true, language: 'en').each do |status| - 2.times { |i| Trends.statuses.add(status, i) } - end - Trends::Statuses.new(threshold: 1, decay_threshold: -1).refresh - end - end - end -end diff --git a/spec/requests/api/v1/trends/tags_spec.rb b/spec/requests/api/v1/trends/tags_spec.rb deleted file mode 100644 index 598f4e7752..0000000000 --- a/spec/requests/api/v1/trends/tags_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V1 Trends Tags' do - describe 'GET /api/v1/trends/tags' do - context 'when trends are disabled' do - before { Setting.trends = false } - - it 'returns http success' do - get '/api/v1/trends/tags' - - expect(response).to have_http_status(200) - expect(response.headers).to_not include('Link') - end - end - - context 'when trends are enabled' do - before { Setting.trends = true } - - it 'returns http success' do - prepare_trends - stub_const('Api::V1::Trends::TagsController::DEFAULT_TAGS_LIMIT', 2) - get '/api/v1/trends/tags' - - expect(response).to have_http_status(200) - expect(response.headers).to include('Link') - end - - def prepare_trends - Fabricate.times(3, :tag, trendable: true).each do |tag| - 2.times { |i| Trends.tags.add(tag, i) } - end - Trends::Tags.new(threshold: 1).refresh - end - end - end -end diff --git a/spec/requests/api/v2/admin/accounts_spec.rb b/spec/requests/api/v2/admin/accounts_spec.rb deleted file mode 100644 index 8f52c6a613..0000000000 --- a/spec/requests/api/v2/admin/accounts_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V2 Admin Accounts' do - let(:role) { UserRole.find_by(name: 'Moderator') } - let(:user) { Fabricate(:user, role: role) } - let(:scopes) { 'admin:read admin:write' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET #index' do - let!(:remote_account) { Fabricate(:account, domain: 'example.org') } - let!(:other_remote_account) { Fabricate(:account, domain: 'foo.bar') } - let!(:suspended_account) { Fabricate(:account, suspended: true) } - let!(:suspended_remote) { Fabricate(:account, domain: 'foo.bar', suspended: true) } - let!(:disabled_account) { Fabricate(:user, disabled: true).account } - let!(:pending_account) { Fabricate(:user, approved: false).account } - let!(:admin_account) { user.account } - - let(:params) { {} } - - before do - pending_account.user.update(approved: false) - - get '/api/v2/admin/accounts', params: params, headers: headers - end - - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' - - context 'when called with status active and origin local and permissions staff' do - let(:params) { { status: 'active', origin: 'local', permissions: 'staff' } } - - it 'returns the correct accounts' do - expect(response).to have_http_status(200) - expect(body_json_ids).to eq([admin_account.id]) - end - end - - context 'when called with by_domain value and origin remote' do - let(:params) { { by_domain: 'example.org', origin: 'remote' } } - - it 'returns the correct accounts' do - expect(response).to have_http_status(200) - expect(body_json_ids).to include(remote_account.id) - expect(body_json_ids).to_not include(other_remote_account.id) - end - end - - context 'when called with status suspended' do - let(:params) { { status: 'suspended' } } - - it 'returns the correct accounts' do - expect(response).to have_http_status(200) - expect(body_json_ids).to include(suspended_remote.id, suspended_account.id) - end - end - - context 'when called with status disabled' do - let(:params) { { status: 'disabled' } } - - it 'returns the correct accounts' do - expect(response).to have_http_status(200) - expect(body_json_ids).to include(disabled_account.id) - end - end - - context 'when called with status pending' do - let(:params) { { status: 'pending' } } - - it 'returns the correct accounts' do - expect(response).to have_http_status(200) - expect(body_json_ids).to include(pending_account.id) - end - end - - def body_json_ids - body_as_json.map { |a| a[:id].to_i } - end - - context 'with limit param' do - let(:params) { { limit: 1 } } - - it 'sets the correct pagination headers' do - expect(response) - .to include_pagination_headers(next: api_v2_admin_accounts_url(limit: 1, max_id: admin_account.id)) - end - end - end -end diff --git a/spec/requests/api/v2/filters/keywords_spec.rb b/spec/requests/api/v2/filters/keywords_spec.rb deleted file mode 100644 index 55fb2afd95..0000000000 --- a/spec/requests/api/v2/filters/keywords_spec.rb +++ /dev/null @@ -1,133 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V2 Filters Keywords' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:filter) { Fabricate(:custom_filter, account: user.account) } - let(:other_user) { Fabricate(:user) } - let(:other_filter) { Fabricate(:custom_filter, account: other_user.account) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v2/filters/:filter_id/keywords' do - let(:scopes) { 'read:filters' } - let!(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) } - - it 'returns http success' do - get "/api/v2/filters/#{filter.id}/keywords", headers: headers - expect(response).to have_http_status(200) - expect(body_as_json) - .to contain_exactly( - include(id: keyword.id.to_s) - ) - end - - context "when trying to access another's user filters" do - it 'returns http not found' do - get "/api/v2/filters/#{other_filter.id}/keywords", headers: headers - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v2/filters/:filter_id/keywords' do - let(:scopes) { 'write:filters' } - let(:filter_id) { filter.id } - - before do - post "/api/v2/filters/#{filter_id}/keywords", headers: headers, params: { keyword: 'magic', whole_word: false } - end - - it 'creates a filter', :aggregate_failures do - expect(response).to have_http_status(200) - - json = body_as_json - expect(json[:keyword]).to eq 'magic' - expect(json[:whole_word]).to be false - - filter = user.account.custom_filters.first - expect(filter).to_not be_nil - expect(filter.keywords.pluck(:keyword)).to eq ['magic'] - end - - context "when trying to add to another another's user filters" do - let(:filter_id) { other_filter.id } - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end - - describe 'GET /api/v2/filters/keywords/:id' do - let(:scopes) { 'read:filters' } - let(:keyword) { Fabricate(:custom_filter_keyword, keyword: 'foo', whole_word: false, custom_filter: filter) } - - before do - get "/api/v2/filters/keywords/#{keyword.id}", headers: headers - end - - it 'responds with the keyword', :aggregate_failures do - expect(response).to have_http_status(200) - - json = body_as_json - expect(json[:keyword]).to eq 'foo' - expect(json[:whole_word]).to be false - end - - context "when trying to access another user's filter keyword" do - let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: other_filter) } - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end - - describe 'PUT /api/v2/filters/keywords/:id' do - let(:scopes) { 'write:filters' } - let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) } - - before do - put "/api/v2/filters/keywords/#{keyword.id}", headers: headers, params: { keyword: 'updated' } - end - - it 'updates the keyword', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(keyword.reload.keyword).to eq 'updated' - end - - context "when trying to update another user's filter keyword" do - let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: other_filter) } - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end - - describe 'DELETE /api/v2/filters/keywords/:id' do - let(:scopes) { 'write:filters' } - let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) } - - before do - delete "/api/v2/filters/keywords/#{keyword.id}", headers: headers - end - - it 'destroys the keyword', :aggregate_failures do - expect(response).to have_http_status(200) - - expect { keyword.reload }.to raise_error ActiveRecord::RecordNotFound - end - - context "when trying to update another user's filter keyword" do - let(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: other_filter) } - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v2/filters/statuses_spec.rb b/spec/requests/api/v2/filters/statuses_spec.rb deleted file mode 100644 index 26d2fb00e1..0000000000 --- a/spec/requests/api/v2/filters/statuses_spec.rb +++ /dev/null @@ -1,109 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'API V2 Filters Statuses' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:filter) { Fabricate(:custom_filter, account: user.account) } - let(:other_user) { Fabricate(:user) } - let(:other_filter) { Fabricate(:custom_filter, account: other_user.account) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v2/filters/:filter_id/statuses' do - let(:scopes) { 'read:filters' } - let!(:status_filter) { Fabricate(:custom_filter_status, custom_filter: filter) } - - it 'returns http success' do - get "/api/v2/filters/#{filter.id}/statuses", headers: headers - expect(response).to have_http_status(200) - expect(body_as_json) - .to contain_exactly( - include(id: status_filter.id.to_s) - ) - end - - context "when trying to access another's user filters" do - it 'returns http not found' do - get "/api/v2/filters/#{other_filter.id}/statuses", headers: headers - expect(response).to have_http_status(404) - end - end - end - - describe 'POST #create' do - let(:scopes) { 'write:filters' } - let(:filter_id) { filter.id } - let!(:status) { Fabricate(:status) } - - before do - post "/api/v2/filters/#{filter_id}/statuses", headers: headers, params: { status_id: status.id } - end - - it 'creates a filter', :aggregate_failures do - expect(response).to have_http_status(200) - - json = body_as_json - expect(json[:status_id]).to eq status.id.to_s - - filter = user.account.custom_filters.first - expect(filter).to_not be_nil - expect(filter.statuses.pluck(:status_id)).to eq [status.id] - end - - context "when trying to add to another another's user filters" do - let(:filter_id) { other_filter.id } - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end - - describe 'GET /api/v2/filters/statuses/:id' do - let(:scopes) { 'read:filters' } - let!(:status_filter) { Fabricate(:custom_filter_status, custom_filter: filter) } - - before do - get "/api/v2/filters/statuses/#{status_filter.id}", headers: headers - end - - it 'responds with the filter', :aggregate_failures do - expect(response).to have_http_status(200) - - json = body_as_json - expect(json[:status_id]).to eq status_filter.status_id.to_s - end - - context "when trying to access another user's filter keyword" do - let(:status_filter) { Fabricate(:custom_filter_status, custom_filter: other_filter) } - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end - - describe 'DELETE /api/v2/filters/statuses/:id' do - let(:scopes) { 'write:filters' } - let(:status_filter) { Fabricate(:custom_filter_status, custom_filter: filter) } - - before do - delete "/api/v2/filters/statuses/#{status_filter.id}", headers: headers - end - - it 'destroys the filter', :aggregate_failures do - expect(response).to have_http_status(200) - - expect { status_filter.reload }.to raise_error ActiveRecord::RecordNotFound - end - - context "when trying to update another user's filter keyword" do - let(:status_filter) { Fabricate(:custom_filter_status, custom_filter: other_filter) } - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v2/filters_spec.rb b/spec/requests/api/v2/filters_spec.rb deleted file mode 100644 index fd0483abbe..0000000000 --- a/spec/requests/api/v2/filters_spec.rb +++ /dev/null @@ -1,248 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Filters' do - let(:user) { Fabricate(:user) } - let(:scopes) { 'read:filters write:filters' } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - shared_examples 'unauthorized for invalid token' do - let(:headers) { { 'Authorization' => '' } } - - it 'returns http unauthorized' do - subject - - expect(response).to have_http_status(401) - end - end - - describe 'GET /api/v2/filters' do - subject do - get '/api/v2/filters', headers: headers - end - - let!(:filters) { Fabricate.times(2, :custom_filter, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'write write:filters' - it_behaves_like 'unauthorized for invalid token' - - it 'returns the existing filters successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.pluck(:id)).to match_array(filters.map { |filter| filter.id.to_s }) - end - end - - describe 'POST /api/v2/filters' do - subject do - post '/api/v2/filters', params: params, headers: headers - end - - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'read read:filters' - it_behaves_like 'unauthorized for invalid token' - - context 'with valid params' do - let(:params) { { title: 'magic', context: %w(home), filter_action: 'hide', keywords_attributes: [keyword: 'magic'] } } - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'returns a filter with keywords', :aggregate_failures do - subject - - json = body_as_json - - expect(json[:title]).to eq 'magic' - expect(json[:filter_action]).to eq 'hide' - expect(json[:context]).to eq ['home'] - expect(json[:keywords].map { |keyword| keyword.slice(:keyword, :whole_word) }).to eq [{ keyword: 'magic', whole_word: true }] - end - - it 'creates a filter', :aggregate_failures do - subject - - filter = user.account.custom_filters.first - - expect(filter).to be_present - expect(filter.keywords.pluck(:keyword)).to eq ['magic'] - expect(filter.context).to eq %w(home) - expect(filter.irreversible?).to be true - expect(filter.expires_at).to be_nil - end - end - - context 'when the required title param is missing' do - let(:params) { { context: %w(home), filter_action: 'hide', keywords_attributes: [keyword: 'magic'] } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the required context param is missing' do - let(:params) { { title: 'magic', filter_action: 'hide', keywords_attributes: [keyword: 'magic'] } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - - context 'when the given context value is invalid' do - let(:params) { { title: 'magic', context: %w(shaolin), filter_action: 'hide', keywords_attributes: [keyword: 'magic'] } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - end - - describe 'GET /api/v2/filters/:id' do - subject do - get "/api/v2/filters/#{filter.id}", headers: headers - end - - let(:filter) { Fabricate(:custom_filter, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'write write:filters' - it_behaves_like 'unauthorized for invalid token' - - it 'returns the filter successfully', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:id]).to eq(filter.id.to_s) - end - - context 'when the filter belongs to someone else' do - let(:filter) { Fabricate(:custom_filter) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'PUT /api/v2/filters/:id' do - subject do - put "/api/v2/filters/#{filter.id}", params: params, headers: headers - end - - let!(:filter) { Fabricate(:custom_filter, account: user.account) } - let!(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) } - let(:params) { {} } - - it_behaves_like 'forbidden for wrong scope', 'read read:filters' - it_behaves_like 'unauthorized for invalid token' - - context 'when updating filter parameters' do - context 'with valid params' do - let(:params) { { title: 'updated', context: %w(home public) } } - - it 'updates the filter successfully', :aggregate_failures do - subject - - filter.reload - - expect(response).to have_http_status(200) - expect(filter.title).to eq 'updated' - expect(filter.reload.context).to eq %w(home public) - end - end - - context 'with invalid params' do - let(:params) { { title: 'updated', context: %w(word) } } - - it 'returns http unprocessable entity' do - subject - - expect(response).to have_http_status(422) - end - end - end - - context 'when updating keywords in bulk' do - let(:params) { { keywords_attributes: [{ id: keyword.id, keyword: 'updated' }] } } - - before do - allow(redis).to receive_messages(publish: nil) - end - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'updates the keyword' do - subject - - expect(keyword.reload.keyword).to eq 'updated' - end - - it 'sends exactly one filters_changed event' do - subject - - expect(redis).to have_received(:publish).with("timeline:#{user.account.id}", Oj.dump(event: :filters_changed)).once - end - end - - context 'when the filter belongs to someone else' do - let(:filter) { Fabricate(:custom_filter) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'DELETE /api/v2/filters/:id' do - subject do - delete "/api/v2/filters/#{filter.id}", headers: headers - end - - let(:filter) { Fabricate(:custom_filter, account: user.account) } - - it_behaves_like 'forbidden for wrong scope', 'read read:filters' - it_behaves_like 'unauthorized for invalid token' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - it 'removes the filter' do - subject - - expect { filter.reload }.to raise_error ActiveRecord::RecordNotFound - end - - context 'when the filter belongs to someone else' do - let(:filter) { Fabricate(:custom_filter) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v2/instance_spec.rb b/spec/requests/api/v2/instance_spec.rb deleted file mode 100644 index 064f92990a..0000000000 --- a/spec/requests/api/v2/instance_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Instances' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v2/instance' do - context 'when logged out' do - it 'returns http success and json' do - get api_v2_instance_path - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - .and include(title: 'Mastodon Glitch Edition') - .and include_configuration_limits - end - end - - context 'when logged in' do - it 'returns http success and json' do - get api_v2_instance_path, headers: headers - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_present - .and include(title: 'Mastodon Glitch Edition') - .and include_configuration_limits - end - end - - def include_configuration_limits - include( - configuration: include( - accounts: include( - max_featured_tags: FeaturedTag::LIMIT, - max_pinned_statuses: StatusPinValidator::PIN_LIMIT - ), - statuses: include( - max_characters: StatusLengthValidator::MAX_CHARS, - max_media_attachments: Status::MEDIA_ATTACHMENTS_LIMIT - ), - polls: include( - max_options: PollValidator::MAX_OPTIONS - ) - ) - ) - end - end -end diff --git a/spec/requests/api/v2/media_spec.rb b/spec/requests/api/v2/media_spec.rb deleted file mode 100644 index 97540413f1..0000000000 --- a/spec/requests/api/v2/media_spec.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Media API', :attachment_processing do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'write' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'POST /api/v2/media' do - context 'when small media format attachment is processed immediately' do - let(:params) { { file: fixture_file_upload('attachment-jpg.123456_abcd', 'image/jpeg') } } - - it 'returns http success' do - post '/api/v2/media', headers: headers, params: params - - expect(File.exist?(user.account.media_attachments.first.file.path(:small))) - .to be true - - expect(response) - .to have_http_status(200) - - expect(body_as_json) - .to be_a(Hash) - end - end - - context 'when large format media attachment has not been processed' do - let(:params) { { file: fixture_file_upload('attachment.webm', 'video/webm') } } - - it 'returns http accepted' do - post '/api/v2/media', headers: headers, params: params - - expect(File.exist?(user.account.media_attachments.first.file.path(:small))) - .to be true - - expect(response) - .to have_http_status(202) - - expect(body_as_json) - .to be_a(Hash) - end - end - - describe 'when paperclip errors occur' do - let(:media_attachments) { double } - let(:params) { { file: fixture_file_upload('attachment.jpg', 'image/jpeg') } } - - before do - allow(User).to receive(:find).with(token.resource_owner_id).and_return(user) - allow(user.account).to receive(:media_attachments).and_return(media_attachments) - end - - context 'when imagemagick cannot identify the file type' do - before do - allow(media_attachments).to receive(:create!).and_raise(Paperclip::Errors::NotIdentifiedByImageMagickError) - end - - it 'returns http unprocessable entity' do - post '/api/v2/media', headers: headers, params: params - - expect(response) - .to have_http_status(422) - - expect(body_as_json) - .to be_a(Hash) - .and include(error: /File type/) - end - end - - context 'when there is a generic error' do - before do - allow(media_attachments).to receive(:create!).and_raise(Paperclip::Error) - end - - it 'returns http 500' do - post '/api/v2/media', headers: headers, params: params - - expect(response) - .to have_http_status(500) - - expect(body_as_json) - .to be_a(Hash) - .and include(error: /processing/) - end - end - end - end -end diff --git a/spec/requests/api/v2/search_spec.rb b/spec/requests/api/v2/search_spec.rb deleted file mode 100644 index 13bcf17984..0000000000 --- a/spec/requests/api/v2/search_spec.rb +++ /dev/null @@ -1,159 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Search API' do - context 'with token' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:search' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v2/search' do - let!(:bob) { Fabricate(:account, username: 'bob_test') } - let!(:ana) { Fabricate(:account, username: 'ana_test') } - let!(:tom) { Fabricate(:account, username: 'tom_test') } - let(:params) { { q: 'test' } } - - it 'returns http success' do - get '/api/v2/search', headers: headers, params: params - - expect(response).to have_http_status(200) - end - - context 'when searching accounts' do - let(:params) { { q: 'test', type: 'accounts' } } - - it 'returns all matching accounts' do - get '/api/v2/search', headers: headers, params: params - - expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(bob.id.to_s, ana.id.to_s, tom.id.to_s) - end - - context 'with truthy `resolve`' do - let(:params) { { q: 'test1', resolve: '1' } } - - it 'returns http unauthorized' do - get '/api/v2/search', headers: headers, params: params - - expect(response).to have_http_status(200) - end - end - - context 'with valid `offset` value' do - let(:params) { { q: 'test1', offset: 1 } } - - it 'returns http unauthorized' do - get '/api/v2/search', headers: headers, params: params - - expect(response).to have_http_status(200) - end - end - - context 'with negative `offset` value' do - let(:params) { { q: 'test1', offset: '-100', type: 'accounts' } } - - it 'returns http bad_request' do - get '/api/v2/search', headers: headers, params: params - - expect(response).to have_http_status(400) - end - end - - context 'with negative `limit` value' do - let(:params) { { q: 'test1', limit: '-100', type: 'accounts' } } - - it 'returns http bad_request' do - get '/api/v2/search', headers: headers, params: params - - expect(response).to have_http_status(400) - end - end - - context 'with following=true' do - let(:params) { { q: 'test', type: 'accounts', following: 'true' } } - - before do - user.account.follow!(ana) - end - - it 'returns only the followed accounts' do - get '/api/v2/search', headers: headers, params: params - - expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(ana.id.to_s) - end - end - end - - context 'when search raises syntax error' do - before { allow(Search).to receive(:new).and_raise(Mastodon::SyntaxError) } - - it 'returns http unprocessable_entity' do - get '/api/v2/search', headers: headers, params: params - - expect(response).to have_http_status(422) - end - end - - context 'when search raises not found error' do - before { allow(Search).to receive(:new).and_raise(ActiveRecord::RecordNotFound) } - - it 'returns http not_found' do - get '/api/v2/search', headers: headers, params: params - - expect(response).to have_http_status(404) - end - end - end - end - - context 'without token' do - describe 'GET /api/v2/search' do - let(:search_params) { nil } - - before do - get '/api/v2/search', params: search_params - end - - context 'without a `q` param' do - it 'returns http bad_request' do - expect(response).to have_http_status(400) - end - end - - context 'with a `q` shorter than 5 characters' do - let(:search_params) { { q: 'test' } } - - it 'returns http success' do - expect(response).to have_http_status(200) - end - end - - context 'with a `q` equal to or longer than 5 characters' do - let(:search_params) { { q: 'test1' } } - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - context 'with truthy `resolve`' do - let(:search_params) { { q: 'test1', resolve: '1' } } - - it 'returns http unauthorized' do - expect(response).to have_http_status(401) - expect(response.body).to match('resolve remote resources') - end - end - - context 'with `offset`' do - let(:search_params) { { q: 'test1', offset: 1 } } - - it 'returns http unauthorized' do - expect(response).to have_http_status(401) - expect(response.body).to match('pagination is not supported') - end - end - end - end - end -end diff --git a/spec/requests/api/v2/suggestions_spec.rb b/spec/requests/api/v2/suggestions_spec.rb deleted file mode 100644 index a7d6a0864f..0000000000 --- a/spec/requests/api/v2/suggestions_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Suggestions API' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v2/suggestions' do - let(:bob) { Fabricate(:account) } - let(:jeff) { Fabricate(:account) } - let(:params) { {} } - - before do - Setting.bootstrap_timeline_accounts = [bob, jeff].map(&:acct).join(',') - end - - it 'returns the expected suggestions' do - get '/api/v2/suggestions', headers: headers - - expect(response).to have_http_status(200) - - expect(body_as_json).to match_array( - [bob, jeff].map do |account| - hash_including({ - source: 'staff', - sources: ['featured'], - account: hash_including({ id: account.id.to_s }), - }) - end - ) - end - end -end diff --git a/spec/requests/api/v2_alpha/notifications_spec.rb b/spec/requests/api/v2_alpha/notifications_spec.rb deleted file mode 100644 index fc1daef43f..0000000000 --- a/spec/requests/api/v2_alpha/notifications_spec.rb +++ /dev/null @@ -1,239 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Notifications' do - let(:user) { Fabricate(:user, account_attributes: { username: 'alice' }) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:scopes) { 'read:notifications write:notifications' } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - describe 'GET /api/v2_alpha/notifications/unread_count', :inline_jobs do - subject do - get '/api/v2_alpha/notifications/unread_count', headers: headers, params: params - end - - let(:params) { {} } - - before do - first_status = PostStatusService.new.call(user.account, text: 'Test') - ReblogService.new.call(Fabricate(:account), first_status) - PostStatusService.new.call(Fabricate(:account), text: 'Hello @alice') - FavouriteService.new.call(Fabricate(:account), first_status) - FavouriteService.new.call(Fabricate(:account), first_status) - FollowService.new.call(Fabricate(:account), user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:notifications' - - context 'with no options' do - it 'returns expected notifications count' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:count]).to eq 4 - end - end - - context 'with a read marker' do - before do - id = user.account.notifications.browserable.order(id: :desc).offset(2).first.id - user.markers.create!(timeline: 'notifications', last_read_id: id) - end - - it 'returns expected notifications count' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:count]).to eq 2 - end - end - - context 'with exclude_types param' do - let(:params) { { exclude_types: %w(mention) } } - - it 'returns expected notifications count' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:count]).to eq 3 - end - end - - context 'with a user-provided limit' do - let(:params) { { limit: 2 } } - - it 'returns a capped value' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:count]).to eq 2 - end - end - - context 'when there are more notifications than the limit' do - before do - stub_const('Api::V2Alpha::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT', 2) - end - - it 'returns a capped value' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:count]).to eq Api::V2Alpha::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT - end - end - end - - describe 'GET /api/v2_alpha/notifications', :inline_jobs do - subject do - get '/api/v2_alpha/notifications', headers: headers, params: params - end - - let(:bob) { Fabricate(:user) } - let(:tom) { Fabricate(:user) } - let(:params) { {} } - - before do - first_status = PostStatusService.new.call(user.account, text: 'Test') - ReblogService.new.call(bob.account, first_status) - mentioning_status = PostStatusService.new.call(bob.account, text: 'Hello @alice') - mentioning_status.mentions.first - FavouriteService.new.call(bob.account, first_status) - FavouriteService.new.call(tom.account, first_status) - FollowService.new.call(bob.account, user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'write write:notifications' - - context 'with no options' do - it 'returns expected notification types', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_json_types).to include('reblog', 'mention', 'favourite', 'follow') - end - end - - context 'with exclude_types param' do - let(:params) { { exclude_types: %w(mention) } } - - it 'returns everything but excluded type', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_as_json.size).to_not eq 0 - expect(body_json_types.uniq).to_not include 'mention' - end - end - - context 'with types param' do - let(:params) { { types: %w(mention) } } - - it 'returns only requested type', :aggregate_failures do - subject - - expect(response).to have_http_status(200) - expect(body_json_types.uniq).to eq ['mention'] - expect(body_as_json.dig(:notification_groups, 0, :page_min_id)).to_not be_nil - end - end - - context 'with limit param' do - let(:params) { { limit: 3 } } - - it 'returns the requested number of notifications paginated', :aggregate_failures do - subject - - notifications = user.account.notifications - - expect(body_as_json[:notification_groups].size) - .to eq(params[:limit]) - - expect(response) - .to include_pagination_headers( - prev: api_v2_alpha_notifications_url(limit: params[:limit], min_id: notifications.last.id), - # TODO: one downside of the current approach is that we return the first ID matching the group, - # not the last that has been skipped, so pagination is very likely to give overlap - next: api_v2_alpha_notifications_url(limit: params[:limit], max_id: notifications[1].id) - ) - end - end - - def body_json_types - body_as_json[:notification_groups].pluck(:type) - end - end - - describe 'GET /api/v2_alpha/notifications/:id' do - subject do - get "/api/v2_alpha/notifications/#{notification.group_key}", headers: headers - end - - let(:notification) { Fabricate(:notification, account: user.account, group_key: 'foobar') } - - it_behaves_like 'forbidden for wrong scope', 'write write:notifications' - - it 'returns http success' do - subject - - expect(response).to have_http_status(200) - end - - context 'when notification belongs to someone else' do - let(:notification) { Fabricate(:notification, group_key: 'foobar') } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v2_alpha/notifications/:id/dismiss' do - subject do - post "/api/v2_alpha/notifications/#{notification.group_key}/dismiss", headers: headers - end - - let!(:notification) { Fabricate(:notification, account: user.account, group_key: 'foobar') } - - it_behaves_like 'forbidden for wrong scope', 'read read:notifications' - - it 'destroys the notification' do - subject - - expect(response).to have_http_status(200) - expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - context 'when notification belongs to someone else' do - let(:notification) { Fabricate(:notification) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - describe 'POST /api/v2_alpha/notifications/clear' do - subject do - post '/api/v2_alpha/notifications/clear', headers: headers - end - - before do - Fabricate(:notification, account: user.account) - end - - it_behaves_like 'forbidden for wrong scope', 'read read:notifications' - - it 'clears notifications for the account' do - subject - - expect(user.account.reload.notifications).to be_empty - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/api/web/embeds_spec.rb b/spec/requests/api/web/embeds_spec.rb deleted file mode 100644 index 0e6195204b..0000000000 --- a/spec/requests/api/web/embeds_spec.rb +++ /dev/null @@ -1,173 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe '/api/web/embed' do - subject { get "/api/web/embeds/#{id}", headers: headers } - - context 'when accessed anonymously' do - let(:headers) { {} } - - context 'when the requested status is local' do - let(:id) { status.id } - - context 'when the requested status is public' do - let(:status) { Fabricate(:status, visibility: :public) } - - it 'returns JSON with an html attribute' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:html]).to be_present - end - end - - context 'when the requested status is private' do - let(:status) { Fabricate(:status, visibility: :private) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - context 'when the requested status is remote' do - let(:remote_account) { Fabricate(:account, domain: 'example.com') } - let(:status) { Fabricate(:status, visibility: :public, account: remote_account, url: 'https://example.com/statuses/1') } - let(:id) { status.id } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when the requested status does not exist' do - let(:id) { -1 } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - context 'with an API token' do - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } - let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } - - context 'when the requested status is local' do - let(:id) { status.id } - - context 'when the requested status is public' do - let(:status) { Fabricate(:status, visibility: :public) } - - it 'returns JSON with an html attribute' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:html]).to be_present - end - - context 'when the requesting user is blocked' do - before do - status.account.block!(user.account) - end - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - context 'when the requested status is private' do - let(:status) { Fabricate(:status, visibility: :private) } - - before do - user.account.follow!(status.account) - end - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - context 'when the requested status is remote' do - let(:remote_account) { Fabricate(:account, domain: 'example.com') } - let(:status) { Fabricate(:status, visibility: :public, account: remote_account, url: 'https://example.com/statuses/1') } - let(:id) { status.id } - - let(:service_instance) { instance_double(FetchOEmbedService) } - - before do - allow(FetchOEmbedService).to receive(:new) { service_instance } - allow(service_instance).to receive(:call) { call_result } - end - - context 'when the requesting user is blocked' do - before do - status.account.block!(user.account) - end - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when successfully fetching OEmbed' do - let(:call_result) { { html: 'ok' } } - - it 'returns JSON with an html attribute' do - subject - - expect(response).to have_http_status(200) - expect(body_as_json[:html]).to be_present - end - end - - context 'when sanitizing the fragment fails' do - let(:call_result) { { html: 'ok' } } - - before { allow(Sanitize).to receive(:fragment).and_raise(ArgumentError) } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - - context 'when failing to fetch OEmbed' do - let(:call_result) { nil } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end - - context 'when the requested status does not exist' do - let(:id) { -1 } - - it 'returns http not found' do - subject - - expect(response).to have_http_status(404) - end - end - end -end diff --git a/spec/requests/backups_spec.rb b/spec/requests/backups_spec.rb deleted file mode 100644 index a6c2efe0db..0000000000 --- a/spec/requests/backups_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Backups' do - include RoutingHelper - - describe 'GET backups#download' do - let(:user) { Fabricate(:user) } - let(:backup) { Fabricate(:backup, user: user) } - - before do - sign_in user - end - - it 'Downloads a user backup' do - get download_backup_path(backup) - - expect(response).to redirect_to(backup_dump_url) - end - - def backup_dump_url - full_asset_url(backup.dump.url) - end - end -end diff --git a/spec/requests/cache_spec.rb b/spec/requests/cache_spec.rb deleted file mode 100644 index 91e5b022e3..0000000000 --- a/spec/requests/cache_spec.rb +++ /dev/null @@ -1,632 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -module TestEndpoints - # Endpoints that do not include authorization-dependent results - # and should be cacheable no matter what. - ALWAYS_CACHED = %w( - /.well-known/host-meta - /.well-known/nodeinfo - /nodeinfo/2.0 - /manifest - /custom.css - /actor - /api/v1/instance/extended_description - /api/v1/instance/rules - /api/v1/instance/peers - /api/v1/instance - /api/v2/instance - ).freeze - - # Endpoints that should be cachable when accessed anonymously but have a Vary - # on Cookie to prevent logged-in users from getting values from logged-out cache. - COOKIE_DEPENDENT_CACHABLE = %w( - / - /explore - /public - /about - /privacy-policy - /directory - /@alice - /@alice/110224538612341312 - /deck/home - ).freeze - - # Endpoints that should be cachable when accessed anonymously but have a Vary - # on Authorization to prevent logged-in users from getting values from logged-out cache. - AUTHORIZATION_DEPENDENT_CACHABLE = %w( - /api/v1/accounts/lookup?acct=alice - /api/v1/statuses/110224538612341312 - /api/v1/statuses/110224538612341312/context - /api/v1/polls/123456789 - /api/v1/trends/statuses - /api/v1/directory - ).freeze - - # Private status that should only be returned with to a valid signature from - # a specific user. - # Should never be cached. - REQUIRE_SIGNATURE = %w( - /users/alice/statuses/110224538643211312 - ).freeze - - # Pages only available to logged-in users. - # Should never be cached. - REQUIRE_LOGIN = %w( - /settings/preferences/appearance - /settings/profile - /settings/featured_tags - /settings/export - /relationships - /filters - /statuses_cleanup - /auth/edit - /oauth/authorized_applications - /admin/dashboard - ).freeze - - # API endpoints only available to logged-in users. - # Should never be cached. - REQUIRE_TOKEN = %w( - /api/v1/announcements - /api/v1/timelines/home - /api/v1/notifications - /api/v1/bookmarks - /api/v1/favourites - /api/v1/follow_requests - /api/v1/conversations - /api/v1/statuses/110224538643211312 - /api/v1/statuses/110224538643211312/context - /api/v1/lists - /api/v2/filters - ).freeze - - # Pages that are only shown to logged-out users, and should never get cached - # because of CSRF protection. - REQUIRE_LOGGED_OUT = %w( - /invite/abcdef - /auth/sign_in - /auth/sign_up - /auth/password/new - /auth/confirmation/new - ).freeze - - # Non-exhaustive list of endpoints that feature language-dependent results - # and thus need to have a Vary on Accept-Language - LANGUAGE_DEPENDENT = %w( - / - /explore - /about - /api/v1/trends/statuses - ).freeze - - module AuthorizedFetch - # Endpoints that require a signature with AUTHORIZED_FETCH and LIMITED_FEDERATION_MODE - # and thus should not be cached in those modes. - REQUIRE_SIGNATURE = %w( - /users/alice - ).freeze - end - - module DisabledAnonymousAPI - # Endpoints that require a signature with DISALLOW_UNAUTHENTICATED_API_ACCESS - # and thus should not be cached in this mode. - REQUIRE_TOKEN = %w( - /api/v1/custom_emojis - ).freeze - end -end - -describe 'Caching behavior' do - shared_examples 'cachable response' do |http_success: false| - it 'does not set cookies or set public cache control', :aggregate_failures do - expect(response.cookies).to be_empty - - # expect(response.cache_control[:max_age]&.to_i).to be_positive - expect(response.cache_control[:public]).to be_truthy - expect(response.cache_control[:private]).to be_falsy - expect(response.cache_control[:no_store]).to be_falsy - expect(response.cache_control[:no_cache]).to be_falsy - - expect(response).to have_http_status(200) if http_success - end - end - - shared_examples 'non-cacheable response' do |http_success: false| - it 'sets private cache control' do - expect(response.cache_control[:private]).to be_truthy - expect(response.cache_control[:no_store]).to be_truthy - - expect(response).to have_http_status(200) if http_success - end - end - - shared_examples 'non-cacheable error' do - it 'does not return HTTP success and does not have cache headers', :aggregate_failures do - expect(response).to_not have_http_status(200) - expect(response.cache_control[:public]).to be_falsy - end - end - - shared_examples 'language-dependent' do - it 'has a Vary on Accept-Language' do - expect(response_vary_headers).to include('accept-language') - end - end - - # Enable CSRF protection like it is in production, as it can cause cookies - # to be set and thus mess with cache. - around do |example| - old = ActionController::Base.allow_forgery_protection - ActionController::Base.allow_forgery_protection = true - - example.run - - ActionController::Base.allow_forgery_protection = old - end - - let(:alice) { Account.find_by(username: 'alice') } - let(:user) { User.find_by(email: 'user@host.example') } - let(:token) { Doorkeeper::AccessToken.find_by(resource_owner_id: user.id) } - - before_all do - alice = Fabricate(:account, username: 'alice') - user = Fabricate(:user, email: 'user@host.example', role: UserRole.find_by(name: 'Moderator')) - status = Fabricate(:status, account: alice, id: 110_224_538_612_341_312) - Fabricate(:status, account: alice, id: 110_224_538_643_211_312, visibility: :private) - Fabricate(:invite, code: 'abcdef') - Fabricate(:poll, status: status, account: alice, id: 123_456_789) - Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') - - user.account.follow!(alice) - end - - context 'when anonymously accessed' do - describe '/users/alice' do - it 'redirects with proper cache header', :aggregate_failures do - get '/users/alice' - - expect(response).to redirect_to('/@alice') - expect(response_vary_headers).to include('accept') - end - end - - TestEndpoints::ALWAYS_CACHED.each do |endpoint| - describe endpoint do - before { get endpoint } - - it_behaves_like 'cachable response' - it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) - end - end - - TestEndpoints::COOKIE_DEPENDENT_CACHABLE.each do |endpoint| - describe endpoint do - before { get endpoint } - - it_behaves_like 'cachable response' - - it 'has a Vary on Cookie' do - expect(response_vary_headers).to include('cookie') - end - - it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) - end - end - - TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint| - describe endpoint do - before { get endpoint } - - it_behaves_like 'cachable response' - - it 'has a Vary on Authorization' do - expect(response_vary_headers).to include('authorization') - end - - it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) - end - end - - TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint| - describe endpoint do - before { get endpoint } - - it_behaves_like 'non-cacheable response' - end - end - - (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::REQUIRE_LOGIN + TestEndpoints::REQUIRE_TOKEN).each do |endpoint| - describe endpoint do - before { get endpoint } - - it_behaves_like 'non-cacheable error' - end - end - - describe '/api/v1/instance/domain_blocks' do - before do - Setting.show_domain_blocks = show_domain_blocks - get '/api/v1/instance/domain_blocks' - end - - context 'when set to be publicly-available' do - let(:show_domain_blocks) { 'all' } - - it_behaves_like 'cachable response' - end - - context 'when allowed for local users only' do - let(:show_domain_blocks) { 'users' } - - it_behaves_like 'non-cacheable error' - end - - context 'when disabled' do - let(:show_domain_blocks) { 'disabled' } - - it_behaves_like 'non-cacheable error' - end - end - end - - context 'when logged in' do - before do - sign_in user, scope: :user - - # Unfortunately, devise's `sign_in` helper causes the `session` to be - # loaded in the next request regardless of whether it's actually accessed - # by the client code. - # - # So, we make an extra query to clear issue a session cookie instead. - # - # A less resource-intensive way to deal with that would be to generate the - # session cookie manually, but this seems pretty involved. - get '/' - end - - TestEndpoints::ALWAYS_CACHED.each do |endpoint| - describe endpoint do - before { get endpoint } - - it_behaves_like 'cachable response' - it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) - end - end - - TestEndpoints::COOKIE_DEPENDENT_CACHABLE.each do |endpoint| - describe endpoint do - before { get endpoint } - - it_behaves_like 'non-cacheable response' - - it 'has a Vary on Cookie' do - expect(response_vary_headers).to include('cookie') - end - end - end - - TestEndpoints::REQUIRE_LOGIN.each do |endpoint| - describe endpoint do - before { get endpoint } - - it_behaves_like 'non-cacheable response', http_success: true - end - end - - TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint| - describe endpoint do - before { get endpoint } - - it_behaves_like 'non-cacheable error' - end - end - end - - context 'with an auth token' do - TestEndpoints::ALWAYS_CACHED.each do |endpoint| - describe endpoint do - before do - get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } - end - - it_behaves_like 'cachable response' - it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) - end - end - - TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint| - describe endpoint do - before do - get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } - end - - it_behaves_like 'non-cacheable response' - - it 'has a Vary on Authorization' do - expect(response_vary_headers).to include('authorization') - end - end - end - - (TestEndpoints::REQUIRE_LOGGED_OUT + TestEndpoints::REQUIRE_TOKEN).each do |endpoint| - describe endpoint do - before do - get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } - end - - it_behaves_like 'non-cacheable response', http_success: true - end - end - - describe '/api/v1/instance/domain_blocks' do - before do - Setting.show_domain_blocks = show_domain_blocks - get '/api/v1/instance/domain_blocks', headers: { 'Authorization' => "Bearer #{token.token}" } - end - - context 'when set to be publicly-available' do - let(:show_domain_blocks) { 'all' } - - it_behaves_like 'cachable response' - end - - context 'when allowed for local users only' do - let(:show_domain_blocks) { 'users' } - - it_behaves_like 'non-cacheable response', http_success: true - end - - context 'when disabled' do - let(:show_domain_blocks) { 'disabled' } - - it_behaves_like 'non-cacheable error' - end - end - end - - context 'with a Signature header' do - let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) } - let(:dummy_signature) { 'dummy-signature' } - - before do - remote_actor.follow!(alice) - end - - describe '/actor' do - before do - get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } - end - - it_behaves_like 'cachable response', http_success: true - end - - TestEndpoints::REQUIRE_SIGNATURE.each do |endpoint| - describe endpoint do - before do - get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } - end - - it_behaves_like 'non-cacheable response', http_success: true - end - end - end - - context 'when enabling AUTHORIZED_FETCH mode' do - around do |example| - ClimateControl.modify AUTHORIZED_FETCH: 'true' do - example.run - end - end - - context 'when not providing a Signature' do - describe '/actor' do - before do - get '/actor', headers: { 'Accept' => 'application/activity+json' } - end - - it_behaves_like 'cachable response', http_success: true - end - - (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| - describe endpoint do - before do - get endpoint, headers: { 'Accept' => 'application/activity+json' } - end - - it_behaves_like 'non-cacheable error' - end - end - end - - context 'when providing a Signature' do - let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) } - let(:dummy_signature) { 'dummy-signature' } - - before do - remote_actor.follow!(alice) - end - - describe '/actor' do - before do - get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } - end - - it_behaves_like 'cachable response', http_success: true - end - - (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| - describe endpoint do - before do - get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } - end - - it_behaves_like 'non-cacheable response', http_success: true - end - end - end - end - - context 'when enabling LIMITED_FEDERATION_MODE mode' do - around do |example| - ClimateControl.modify LIMITED_FEDERATION_MODE: 'true' do - old_limited_federation_mode = Rails.configuration.x.limited_federation_mode - Rails.configuration.x.limited_federation_mode = true - - example.run - - Rails.configuration.x.limited_federation_mode = old_limited_federation_mode - end - end - - context 'when not providing a Signature' do - describe '/actor' do - before do - get '/actor', headers: { 'Accept' => 'application/activity+json' } - end - - it_behaves_like 'cachable response', http_success: true - end - - (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| - describe endpoint do - before do - get endpoint, headers: { 'Accept' => 'application/activity+json' } - end - - it_behaves_like 'non-cacheable error' - end - end - end - - context 'when providing a Signature from an allowed domain' do - let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) } - let(:dummy_signature) { 'dummy-signature' } - - before do - DomainAllow.create!(domain: remote_actor.domain) - remote_actor.follow!(alice) - end - - describe '/actor' do - before do - get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } - end - - it_behaves_like 'cachable response', http_success: true - end - - (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| - describe endpoint do - before do - get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } - end - - it_behaves_like 'non-cacheable response', http_success: true - end - end - end - - context 'when providing a Signature from a non-allowed domain' do - let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) } - let(:dummy_signature) { 'dummy-signature' } - - describe '/actor' do - before do - get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } - end - - it_behaves_like 'cachable response', http_success: true - end - - (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| - describe endpoint do - before do - get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } - end - - it_behaves_like 'non-cacheable error' - end - end - end - end - - context 'when enabling DISALLOW_UNAUTHENTICATED_API_ACCESS' do - around do |example| - ClimateControl.modify DISALLOW_UNAUTHENTICATED_API_ACCESS: 'true' do - example.run - end - end - - context 'when anonymously accessed' do - TestEndpoints::ALWAYS_CACHED.each do |endpoint| - describe endpoint do - before { get endpoint } - - it_behaves_like 'cachable response' - it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) - end - end - - TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint| - describe endpoint do - before { get endpoint } - - it_behaves_like 'non-cacheable response' - end - end - - (TestEndpoints::REQUIRE_TOKEN + TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE + TestEndpoints::DisabledAnonymousAPI::REQUIRE_TOKEN).each do |endpoint| - describe endpoint do - before { get endpoint } - - it_behaves_like 'non-cacheable error' - end - end - end - - context 'with an auth token' do - TestEndpoints::ALWAYS_CACHED.each do |endpoint| - describe endpoint do - before do - get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } - end - - it_behaves_like 'cachable response' - it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) - end - end - - TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint| - describe endpoint do - before do - get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } - end - - it_behaves_like 'non-cacheable response' - - it 'has a Vary on Authorization' do - expect(response_vary_headers).to include('authorization') - end - end - end - - (TestEndpoints::REQUIRE_LOGGED_OUT + TestEndpoints::REQUIRE_TOKEN + TestEndpoints::DisabledAnonymousAPI::REQUIRE_TOKEN).each do |endpoint| - describe endpoint do - before do - get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } - end - - it_behaves_like 'non-cacheable response', http_success: true - end - end - end - end - - private - - def response_vary_headers - response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase } - end -end diff --git a/spec/requests/catch_all_route_request_spec.rb b/spec/requests/catch_all_route_request_spec.rb deleted file mode 100644 index e600bedfe0..0000000000 --- a/spec/requests/catch_all_route_request_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'The catch all route' do - describe 'with a simple value' do - it 'returns a 404 page as html' do - get '/test' - - expect(response).to have_http_status 404 - expect(response.media_type).to eq 'text/html' - end - end - - describe 'with an implied format' do - it 'returns a 404 page as html' do - get '/test.test' - - expect(response).to have_http_status 404 - expect(response.media_type).to eq 'text/html' - end - end -end diff --git a/spec/requests/content_security_policy_spec.rb b/spec/requests/content_security_policy_spec.rb deleted file mode 100644 index 4abe3c2113..0000000000 --- a/spec/requests/content_security_policy_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Content-Security-Policy' do - before { allow(SecureRandom).to receive(:base64).with(16).and_return('ZbA+JmE7+bK8F5qvADZHuQ==') } - - it 'sets the expected CSP headers' do - get '/' - - expect(response_csp_headers) - .to match_array(expected_csp_headers) - end - - def response_csp_headers - response - .headers['Content-Security-Policy'] - .split(';') - .map(&:strip) - end - - def expected_csp_headers - <<~CSP.split("\n").map(&:strip) - base-uri 'none' - child-src 'self' blob: https://cb6e6126.ngrok.io - connect-src 'self' data: blob: https://cb6e6126.ngrok.io #{Rails.configuration.x.streaming_api_base_url} https://media.tenor.com https://api.tenor.com - default-src 'none' - font-src 'self' https://cb6e6126.ngrok.io - form-action 'self' - frame-ancestors 'none' - frame-src 'self' https: - img-src 'self' data: blob: https://cb6e6126.ngrok.io https://media.tenor.com - manifest-src 'self' https://cb6e6126.ngrok.io - media-src 'self' data: https://cb6e6126.ngrok.io https://media.tenor.com - script-src 'self' https://cb6e6126.ngrok.io 'wasm-unsafe-eval' - style-src 'self' https://cb6e6126.ngrok.io 'nonce-ZbA+JmE7+bK8F5qvADZHuQ==' - worker-src 'self' blob: https://cb6e6126.ngrok.io - CSP - end -end diff --git a/spec/requests/custom_css_spec.rb b/spec/requests/custom_css_spec.rb deleted file mode 100644 index 5271ed4a5a..0000000000 --- a/spec/requests/custom_css_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Custom CSS' do - include RoutingHelper - - describe 'GET /custom.css' do - context 'without any CSS or User Roles' do - it 'returns empty stylesheet' do - get '/custom.css' - - expect(response.content_type).to include('text/css') - expect(response.body.presence).to be_nil - end - end - - context 'with CSS settings' do - before do - Setting.custom_css = expected_css - end - - it 'returns stylesheet from settings' do - get '/custom.css' - - expect(response.content_type).to include('text/css') - expect(response.body.strip).to eq(expected_css) - end - - def expected_css - <<~CSS.strip - body { background-color: red; } - CSS - end - end - - context 'with highlighted colored UserRole records' do - before do - _highlighted_colored = Fabricate :user_role, highlighted: true, color: '#336699', id: '123_123_123' - _highlighted_no_color = Fabricate :user_role, highlighted: true, color: '' - _no_highlight_with_color = Fabricate :user_role, highlighted: false, color: '' - end - - it 'returns stylesheet from settings' do - get '/custom.css' - - expect(response.content_type).to include('text/css') - expect(response.body.strip).to eq(expected_css) - end - - def expected_css - <<~CSS.strip - .user-role-123123123 { - --user-role-accent: #336699; - } - CSS - end - end - end -end diff --git a/spec/requests/disabled_oauth_endpoints_spec.rb b/spec/requests/disabled_oauth_endpoints_spec.rb deleted file mode 100644 index 7c2c09f380..0000000000 --- a/spec/requests/disabled_oauth_endpoints_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Disabled OAuth routes' do - # These routes are disabled via the doorkeeper configuration for - # `admin_authenticator`, as these routes should only be accessible by server - # administrators. For now, these routes are not properly designed and - # integrated into Mastodon, so we're disabling them completely - describe 'GET /oauth/applications' do - it 'returns 403 forbidden' do - get oauth_applications_path - - expect(response).to have_http_status(403) - end - end - - describe 'POST /oauth/applications' do - it 'returns 403 forbidden' do - post oauth_applications_path - - expect(response).to have_http_status(403) - end - end - - describe 'GET /oauth/applications/new' do - it 'returns 403 forbidden' do - get new_oauth_application_path - - expect(response).to have_http_status(403) - end - end - - describe 'GET /oauth/applications/:id' do - let(:application) { Fabricate(:application, scopes: 'read') } - - it 'returns 403 forbidden' do - get oauth_application_path(application) - - expect(response).to have_http_status(403) - end - end - - describe 'PATCH /oauth/applications/:id' do - let(:application) { Fabricate(:application, scopes: 'read') } - - it 'returns 403 forbidden' do - patch oauth_application_path(application) - - expect(response).to have_http_status(403) - end - end - - describe 'PUT /oauth/applications/:id' do - let(:application) { Fabricate(:application, scopes: 'read') } - - it 'returns 403 forbidden' do - put oauth_application_path(application) - - expect(response).to have_http_status(403) - end - end - - describe 'DELETE /oauth/applications/:id' do - let(:application) { Fabricate(:application, scopes: 'read') } - - it 'returns 403 forbidden' do - delete oauth_application_path(application) - - expect(response).to have_http_status(403) - end - end - - describe 'GET /oauth/applications/:id/edit' do - let(:application) { Fabricate(:application, scopes: 'read') } - - it 'returns 403 forbidden' do - get edit_oauth_application_path(application) - - expect(response).to have_http_status(403) - end - end -end diff --git a/spec/requests/follower_accounts_spec.rb b/spec/requests/follower_accounts_spec.rb deleted file mode 100644 index 52e86e13fe..0000000000 --- a/spec/requests/follower_accounts_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'FollowerAccountsController' do - describe 'The follower_accounts route' do - it "returns a http 'moved_permanently' code" do - get '/users/:username/followers' - - expect(response).to have_http_status(301) - end - end -end diff --git a/spec/requests/following_accounts_spec.rb b/spec/requests/following_accounts_spec.rb deleted file mode 100644 index f0955ceb36..0000000000 --- a/spec/requests/following_accounts_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'FollowingAccountsController' do - describe 'The following_accounts route' do - it "returns a http 'moved_permanently' code" do - get '/users/:username/following' - - expect(response).to have_http_status(301) - end - end -end diff --git a/spec/requests/invite_spec.rb b/spec/requests/invite_spec.rb deleted file mode 100644 index c44ef2419c..0000000000 --- a/spec/requests/invite_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'invites' do - let(:invite) { Fabricate(:invite) } - - context 'when requesting a JSON document' do - it 'returns a JSON document with expected attributes' do - get "/invite/#{invite.code}", headers: { 'Accept' => 'application/activity+json' } - - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/json' - - expect(body_as_json[:invite_code]).to eq invite.code - end - end - - context 'when not requesting a JSON document' do - it 'returns an HTML page' do - get "/invite/#{invite.code}" - - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'text/html' - end - end -end diff --git a/spec/requests/link_headers_spec.rb b/spec/requests/link_headers_spec.rb deleted file mode 100644 index b822adbfb8..0000000000 --- a/spec/requests/link_headers_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Link headers' do - describe 'on the account show page' do - let(:account) { Fabricate(:account, username: 'test') } - - before do - get short_account_path(username: account) - end - - it 'contains webfinger url in link header' do - link_header = link_header_with_type('application/jrd+json') - - expect(link_header.href).to eq 'http://www.example.com/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io' - expect(link_header.attr_pairs.first).to eq %w(rel lrdd) - end - - it 'contains activitypub url in link header' do - link_header = link_header_with_type('application/activity+json') - - expect(link_header.href).to eq 'https://cb6e6126.ngrok.io/users/test' - expect(link_header.attr_pairs.first).to eq %w(rel alternate) - end - - def link_header_with_type(type) - LinkHeader.parse(response.headers['Link'].to_s).links.find do |link| - link.attr_pairs.any?(['type', type]) - end - end - end -end diff --git a/spec/requests/localization_spec.rb b/spec/requests/localization_spec.rb deleted file mode 100644 index b7fb53ed8d..0000000000 --- a/spec/requests/localization_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Localization' do - around do |example| - I18n.with_locale(I18n.locale) do - example.run - end - end - - it 'uses a specific region when provided' do - headers = { 'Accept-Language' => 'zh-HK' } - - get '/auth/sign_in', headers: headers - - expect(response.body).to include( - I18n.t('auth.login', locale: 'zh-HK') - ) - end - - it 'falls back to a locale when region missing' do - headers = { 'Accept-Language' => 'es-FAKE' } - - get '/auth/sign_in', headers: headers - - expect(response.body).to include( - I18n.t('auth.login', locale: 'es') - ) - end - - it 'falls back to english when locale is missing' do - headers = { 'Accept-Language' => '12-FAKE' } - - get '/auth/sign_in', headers: headers - - expect(response.body).to include( - I18n.t('auth.login', locale: 'en') - ) - end -end diff --git a/spec/requests/mail_subscriptions_spec.rb b/spec/requests/mail_subscriptions_spec.rb deleted file mode 100644 index cc6557cab0..0000000000 --- a/spec/requests/mail_subscriptions_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'MailSubscriptionsController' do - let(:user) { Fabricate(:user) } - let(:token) { user.to_sgid(for: 'unsubscribe').to_s } - let(:type) { 'follow' } - - shared_examples 'not found with invalid token' do - context 'with invalid token' do - let(:token) { 'invalid-token' } - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end - - shared_examples 'not found with invalid type' do - context 'with invalid type' do - let(:type) { 'invalid_type' } - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end - - describe 'on the unsubscribe confirmation page' do - before do - get unsubscribe_url(token: token, type: type) - end - - it_behaves_like 'not found with invalid token' - it_behaves_like 'not found with invalid type' - - it 'shows unsubscribe form' do - expect(response).to have_http_status(200) - - expect(response.body).to include( - I18n.t('mail_subscriptions.unsubscribe.action') - ) - expect(response.body).to include(user.email) - end - end - - describe 'submitting the unsubscribe confirmation page' do - before do - user.settings.update('notification_emails.follow': true) - user.save! - - post unsubscribe_url, params: { token: token, type: type } - end - - it_behaves_like 'not found with invalid token' - it_behaves_like 'not found with invalid type' - - it 'shows confirmation page' do - expect(response).to have_http_status(200) - - expect(response.body).to include( - I18n.t('mail_subscriptions.unsubscribe.complete') - ) - expect(response.body).to include(user.email) - end - - it 'updates notification settings' do - user.reload - expect(user.settings['notification_emails.follow']).to be false - end - end - - describe 'unsubscribing with List-Unsubscribe-Post' do - around do |example| - old = ActionController::Base.allow_forgery_protection - ActionController::Base.allow_forgery_protection = true - - example.run - - ActionController::Base.allow_forgery_protection = old - end - - before do - user.settings.update('notification_emails.follow': true) - user.save! - - post unsubscribe_url(token: token, type: type), params: { 'List-Unsubscribe' => 'One-Click' } - end - - it_behaves_like 'not found with invalid token' - it_behaves_like 'not found with invalid type' - - it 'return http success' do - expect(response).to have_http_status(200) - end - - it 'updates notification settings' do - user.reload - expect(user.settings['notification_emails.follow']).to be false - end - end -end diff --git a/spec/requests/omniauth_callbacks_spec.rb b/spec/requests/omniauth_callbacks_spec.rb deleted file mode 100644 index 095535e485..0000000000 --- a/spec/requests/omniauth_callbacks_spec.rb +++ /dev/null @@ -1,143 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'OmniAuth callbacks' do - shared_examples 'omniauth provider callbacks' do |provider| - subject { post send :"user_#{provider}_omniauth_callback_path" } - - context 'with full information in response' do - before do - mock_omniauth(provider, { - provider: provider.to_s, - uid: '123', - info: { - verified: 'true', - email: 'user@host.example', - }, - }) - end - - context 'without a matching user' do - it 'creates a user and an identity and redirects to root path' do - expect { subject } - .to change(User, :count) - .by(1) - .and change(Identity, :count) - .by(1) - .and change(LoginActivity, :count) - .by(1) - - expect(User.last.email).to eq('user@host.example') - expect(Identity.find_by(user: User.last).uid).to eq('123') - expect(response).to redirect_to(root_path) - end - end - - context 'with a matching user and no matching identity' do - before do - Fabricate(:user, email: 'user@host.example') - end - - context 'when ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH is set to true' do - around do |example| - ClimateControl.modify ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH: 'true' do - example.run - end - end - - it 'matches the existing user, creates an identity, and redirects to root path' do - expect { subject } - .to not_change(User, :count) - .and change(Identity, :count) - .by(1) - .and change(LoginActivity, :count) - .by(1) - - expect(Identity.find_by(user: User.last).uid).to eq('123') - expect(response).to redirect_to(root_path) - end - end - - context 'when ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH is not set to true' do - it 'does not match the existing user or create an identity, and redirects to login page' do - expect { subject } - .to not_change(User, :count) - .and not_change(Identity, :count) - .and not_change(LoginActivity, :count) - - expect(response).to redirect_to(new_user_session_url) - end - end - end - - context 'with a matching user and a matching identity' do - before do - user = Fabricate(:user, email: 'user@host.example') - Fabricate(:identity, user: user, uid: '123', provider: provider) - end - - it 'matches the existing records and redirects to root path' do - expect { subject } - .to not_change(User, :count) - .and not_change(Identity, :count) - .and change(LoginActivity, :count) - .by(1) - - expect(response).to redirect_to(root_path) - end - end - end - - context 'with a response missing email address' do - before do - mock_omniauth(provider, { - provider: provider.to_s, - uid: '123', - info: { - verified: 'true', - }, - }) - end - - it 'redirects to the auth setup page' do - expect { subject } - .to change(User, :count) - .by(1) - .and change(Identity, :count) - .by(1) - .and change(LoginActivity, :count) - .by(1) - - expect(response).to redirect_to(auth_setup_path(missing_email: '1')) - end - end - - context 'when a user cannot be built' do - before do - allow(User).to receive(:find_for_omniauth).and_return(User.new) - end - - it 'redirects to the new user signup page' do - expect { subject } - .to not_change(User, :count) - .and not_change(Identity, :count) - .and not_change(LoginActivity, :count) - - expect(response).to redirect_to(new_user_registration_url) - end - end - end - - describe '#openid_connect', if: ENV['OIDC_ENABLED'] == 'true' && ENV['OIDC_SCOPE'].present? do - include_examples 'omniauth provider callbacks', :openid_connect - end - - describe '#cas', if: ENV['CAS_ENABLED'] == 'true' do - include_examples 'omniauth provider callbacks', :cas - end - - describe '#saml', if: ENV['SAML_ENABLED'] == 'true' do - include_examples 'omniauth provider callbacks', :saml - end -end diff --git a/spec/requests/remote_interaction_helper_spec.rb b/spec/requests/remote_interaction_helper_spec.rb deleted file mode 100644 index e6364fe8ce..0000000000 --- a/spec/requests/remote_interaction_helper_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Remote Interaction Helper' do - describe 'GET /remote_interaction_helper' do - it 'returns http success' do - get remote_interaction_helper_path - - expect(response) - .to have_http_status(200) - .and render_template(:index, layout: 'helper_frame') - .and have_attributes( - headers: include( - 'X-Frame-Options' => 'SAMEORIGIN', - 'Referrer-Policy' => 'no-referrer', - 'Content-Security-Policy' => expected_csp_headers - ) - ) - end - end - - private - - def expected_csp_headers - <<~CSP.squish - default-src 'none'; frame-ancestors 'self'; form-action 'none'; script-src 'self' https://cb6e6126.ngrok.io 'wasm-unsafe-eval'; connect-src https: - CSP - end -end diff --git a/spec/requests/self_destruct_spec.rb b/spec/requests/self_destruct_spec.rb deleted file mode 100644 index f71a2325e2..0000000000 --- a/spec/requests/self_destruct_spec.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Self-destruct mode' do - before do - allow(SelfDestructHelper).to receive(:self_destruct?).and_return(true) - end - - shared_examples 'generic logged out request' do |path| - it 'returns 410 gone and mentions self-destruct' do - get path, headers: { 'Accept' => 'text/html' } - - expect(response).to have_http_status(410) - expect(response.body).to include(I18n.t('self_destruct.title')) - end - end - - shared_examples 'accessible logged-in endpoint' do |path| - it 'returns 200 ok' do - get path - - expect(response).to have_http_status(200) - end - end - - shared_examples 'ActivityPub request' do |path| - context 'without signature' do - it 'returns 410 gone' do - get path, headers: { - 'Accept' => 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - } - - expect(response).to have_http_status(410) - end - end - - context 'with invalid signature' do - it 'returns 410 gone' do - get path, headers: { - 'Accept' => 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="bar"', - } - - expect(response).to have_http_status(410) - end - end - end - - context 'when requesting various unavailable endpoints' do - it_behaves_like 'generic logged out request', '/' - it_behaves_like 'generic logged out request', '/about' - it_behaves_like 'generic logged out request', '/public' - end - - context 'when requesting a suspended account' do - let(:suspended) { Fabricate(:account, username: 'suspended') } - - before do - suspended.suspend! - end - - it_behaves_like 'generic logged out request', '/@suspended' - it_behaves_like 'ActivityPub request', '/users/suspended' - it_behaves_like 'ActivityPub request', '/users/suspended/followers' - it_behaves_like 'ActivityPub request', '/users/suspended/outbox' - end - - context 'when requesting a non-suspended account' do - before do - Fabricate(:account, username: 'bob') - end - - it_behaves_like 'generic logged out request', '/@bob' - it_behaves_like 'ActivityPub request', '/users/bob' - it_behaves_like 'ActivityPub request', '/users/bob/followers' - it_behaves_like 'ActivityPub request', '/users/bob/outbox' - end - - context 'when accessing still-enabled endpoints when logged in' do - let(:user) { Fabricate(:user) } - - before do - sign_in(user) - end - - it_behaves_like 'accessible logged-in endpoint', '/auth/edit' - it_behaves_like 'accessible logged-in endpoint', '/settings/export' - it_behaves_like 'accessible logged-in endpoint', '/settings/login_activities' - it_behaves_like 'accessible logged-in endpoint', '/settings/exports/follows.csv' - end -end diff --git a/spec/requests/signature_verification_spec.rb b/spec/requests/signature_verification_spec.rb deleted file mode 100644 index 401828c4a3..0000000000 --- a/spec/requests/signature_verification_spec.rb +++ /dev/null @@ -1,398 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'signature verification concern' do - before do - stub_tests_controller - - # Signature checking is time-dependent, so travel to a fixed date - travel_to '2023-12-20T10:00:00Z' - end - - after { Rails.application.reload_routes! } - - # Include the private key so the tests can be easily adjusted and reviewed - let(:actor_keypair) do - OpenSSL::PKey.read(<<~PEM_TEXT) - -----BEGIN RSA PRIVATE KEY----- - MIIEowIBAAKCAQEAqIAYvNFGbZ5g4iiK6feSdXD4bDStFM58A7tHycYXaYtzZQpI - eHXAmaXuZzXIwtrP4N0gIk8JNwZvXj2UPS+S07t0V9wNK94he01LV5EMz/GN4eNn - FmDL64HIEuKLvV8TvgjbUPRD6Y5X0UpKi2ZIFLSb96Q5w0Z/k7ntpVKV52y8kz5F - jr/O/0JuHryZe0yItzJh8kzFfeMf0EXzfSnaKvT7P9jhgC6uTre+jXyvVZjiHDrn - qvvucdI3I7DRfXo1OqARBrLjy+TdseUAjNYJ+OuPRI1URIWQI01DCHqcohVu9+Ar - +BiCjFp3ua+XMuJvrvbD61d1Fvig/9nbBRR+8QIDAQABAoIBAAgySHnFWI6gItR3 - fkfiqIm80cHCN3Xk1C6iiVu+3oBOZbHpW9R7vl9e/WOA/9O+LPjiSsQOegtWnVvd - RRjrl7Hj20VDlZKv5Mssm6zOGAxksrcVbqwdj+fUJaNJCL0AyyseH0x/IE9T8rDC - I1GH+3tB3JkhkIN/qjipdX5ab8MswEPu8IC4ViTpdBgWYY/xBcAHPw4xuL0tcwzh - FBlf4DqoEVQo8GdK5GAJ2Ny0S4xbXHUURzx/R4y4CCts7niAiLGqd9jmLU1kUTMk - QcXfQYK6l+unLc7wDYAz7sFEHh04M48VjWwiIZJnlCqmQbLda7uhhu8zkF1DqZTu - ulWDGQECgYEA0TIAc8BQBVab979DHEEmMdgqBwxLY3OIAk0b+r50h7VBGWCDPRsC - STD73fQY3lNet/7/jgSGwwAlAJ5PpMXxXiZAE3bUwPmHzgF7pvIOOLhA8O07tHSO - L2mvQe6NPzjZ+6iAO2U9PkClxcvGvPx2OBvisfHqZLmxC9PIVxzruQECgYEAzjM6 - BTUXa6T/qHvLFbN699BXsUOGmHBGaLRapFDBfVvgZrwqYQcZpBBhesLdGTGSqwE7 - gWsITPIJ+Ldo+38oGYyVys+w/V67q6ud7hgSDTW3hSvm+GboCjk6gzxlt9hQ0t9X - 8vfDOYhEXvVUJNv3mYO60ENqQhILO4bQ0zi+VfECgYBb/nUccfG+pzunU0Cb6Dp3 - qOuydcGhVmj1OhuXxLFSDG84Tazo7juvHA9mp7VX76mzmDuhpHPuxN2AzB2SBEoE - cSW0aYld413JRfWukLuYTc6hJHIhBTCRwRQFFnae2s1hUdQySm8INT2xIc+fxBXo - zrp+Ljg5Wz90SAnN5TX0AQKBgDaatDOq0o/r+tPYLHiLtfWoE4Dau+rkWJDjqdk3 - lXWn/e3WyHY3Vh/vQpEqxzgju45TXjmwaVtPATr+/usSykCxzP0PMPR3wMT+Rm1F - rIoY/odij+CaB7qlWwxj0x/zRbwB7x1lZSp4HnrzBpxYL+JUUwVRxPLIKndSBTza - GvVRAoGBAIVBcNcRQYF4fvZjDKAb4fdBsEuHmycqtRCsnkGOz6ebbEQznSaZ0tZE - +JuouZaGjyp8uPjNGD5D7mIGbyoZ3KyG4mTXNxDAGBso1hrNDKGBOrGaPhZx8LgO - 4VXJ+ybXrATf4jr8ccZYsZdFpOphPzz+j55Mqg5vac5P1XjmsGTb - -----END RSA PRIVATE KEY----- - PEM_TEXT - end - - context 'without a Signature header' do - it 'does not treat the request as signed' do - get '/activitypub/success' - - expect(response).to have_http_status(200) - expect(body_as_json).to match( - signed_request: false, - signature_actor_id: nil, - error: 'Request not signed' - ) - end - - context 'when a signature is required' do - it 'returns http unauthorized with appropriate error' do - get '/activitypub/signature_required' - - expect(response).to have_http_status(401) - expect(body_as_json).to match( - error: 'Request not signed' - ) - end - end - end - - context 'with an HTTP Signature from a known account' do - let!(:actor) { Fabricate(:account, domain: 'remote.domain', uri: 'https://remote.domain/users/bob', private_key: nil, public_key: actor_keypair.public_key.to_pem) } - - context 'with a valid signature on a GET request' do - let(:signature_header) do - 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="' # rubocop:disable Layout/LineLength - end - - it 'successfuly verifies signature', :aggregate_failures do - expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) - - get '/activitypub/success', headers: { - 'Host' => 'www.example.com', - 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', - 'Signature' => signature_header, - } - - expect(response).to have_http_status(200) - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: actor.id.to_s - ) - end - end - - context 'with a valid signature on a GET request that has a query string' do - let(:signature_header) do - 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="SDMa4r/DQYMXYxVgYO2yEqGWWUXugKjVuz0I8dniQAk+aunzBaF2aPu+4grBfawAshlx1Xytl8lhb0H2MllEz16/tKY7rUrb70MK0w8ohXgpb0qs3YvQgdj4X24L1x2MnkFfKHR/J+7TBlnivq0HZqXm8EIkPWLv+eQxu8fbowLwHIVvRd/3t6FzvcfsE0UZKkoMEX02542MhwSif6cu7Ec/clsY9qgKahb9JVGOGS1op9Lvg/9y1mc8KCgD83U5IxVygYeYXaVQ6gixA9NgZiTCwEWzHM5ELm7w5hpdLFYxYOHg/3G3fiqJzpzNQAcCD4S4JxfE7hMI0IzVlNLT6A=="' # rubocop:disable Layout/LineLength - end - - it 'successfuly verifies signature', :aggregate_failures do - expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success?foo=42', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) - - get '/activitypub/success?foo=42', headers: { - 'Host' => 'www.example.com', - 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', - 'Signature' => signature_header, - } - - expect(response).to have_http_status(200) - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: actor.id.to_s - ) - end - end - - context 'when the query string is missing from the signature verification (compatibility quirk)' do - let(:signature_header) do - 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="' # rubocop:disable Layout/LineLength - end - - it 'successfuly verifies signature', :aggregate_failures do - expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) - - get '/activitypub/success?foo=42', headers: { - 'Host' => 'www.example.com', - 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', - 'Signature' => signature_header, - } - - expect(response).to have_http_status(200) - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: actor.id.to_s - ) - end - end - - context 'with mismatching query string' do - let(:signature_header) do - 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="SDMa4r/DQYMXYxVgYO2yEqGWWUXugKjVuz0I8dniQAk+aunzBaF2aPu+4grBfawAshlx1Xytl8lhb0H2MllEz16/tKY7rUrb70MK0w8ohXgpb0qs3YvQgdj4X24L1x2MnkFfKHR/J+7TBlnivq0HZqXm8EIkPWLv+eQxu8fbowLwHIVvRd/3t6FzvcfsE0UZKkoMEX02542MhwSif6cu7Ec/clsY9qgKahb9JVGOGS1op9Lvg/9y1mc8KCgD83U5IxVygYeYXaVQ6gixA9NgZiTCwEWzHM5ELm7w5hpdLFYxYOHg/3G3fiqJzpzNQAcCD4S4JxfE7hMI0IzVlNLT6A=="' # rubocop:disable Layout/LineLength - end - - it 'fails to verify signature', :aggregate_failures do - expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success?foo=42', { 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) - - get '/activitypub/success?foo=43', headers: { - 'Host' => 'www.example.com', - 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', - 'Signature' => signature_header, - } - - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: nil, - error: anything - ) - end - end - - context 'with a mismatching path' do - it 'fails to verify signature', :aggregate_failures do - get '/activitypub/alternative-path', headers: { - 'Host' => 'www.example.com', - 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', - 'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength - } - - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: nil, - error: anything - ) - end - end - - context 'with a mismatching method' do - it 'fails to verify signature', :aggregate_failures do - post '/activitypub/success', headers: { - 'Host' => 'www.example.com', - 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', - 'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength - } - - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: nil, - error: anything - ) - end - end - - context 'with an unparsable date' do - let(:signature_header) do - 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="d4B7nfx8RJcfdJDu1J//5WzPzK/hgtPkdzZx49lu5QhnE7qdV3lgyVimmhCFrO16bwvzIp9iRMyRLkNFxLiEeVaa1gqeKbldGSnU0B0OMjx7rFBa65vLuzWQOATDitVGiBEYqoK4v0DMuFCz2DtFaA/DIUZ3sty8bZ/Ea3U1nByLOO6MacARA3zhMSI0GNxGqsSmZmG0hPLavB3jIXoE3IDoQabMnC39jrlcO/a8h1iaxBm2WD8TejrImJullgqlJIFpKhIHI3ipQkvTGPlm9dx0y+beM06qBvWaWQcmT09eRIUefVsOAzIhUtS/7FVb/URhZvircIJDa7vtiFcmZQ=="' # rubocop:disable Layout/LineLength - end - - it 'fails to verify signature', :aggregate_failures do - expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'wrong date', 'Host' => 'www.example.com' }) - - get '/activitypub/success', headers: { - 'Host' => 'www.example.com', - 'Date' => 'wrong date', - 'Signature' => signature_header, - } - - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: nil, - error: 'Invalid Date header: not RFC 2616 compliant date: "wrong date"' - ) - end - end - - context 'with a request older than a day' do - let(:signature_header) do - 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="G1NuJv4zgoZ3B/ZIjzDWZHK4RC+5pYee74q8/LJEMCWXhcnAomcb9YHaqk1QYfQvcBUIXw3UZ3Q9xO8F9y0i8G5mzJHfQ+OgHqCoJk8EmGwsUXJMh5s1S5YFCRt8TT12TmJZz0VMqLq85ubueSYBM7QtUE/FzFIVLvz4RysgXxaXQKzdnM6+gbUEEKdCURpXdQt2NXQhp4MAmZH3+0lQoR6VxdsK0hx0Ji2PNp1nuqFTlYqNWZazVdLBN+9rETLRmvGXknvg9jOxTTppBVWnkAIl26HtLS3wwFVvz4pJzi9OQDOvLziehVyLNbU61hky+oJ215e2HuKSe2hxHNl1MA=="' # rubocop:disable Layout/LineLength - end - - it 'fails to verify signature', :aggregate_failures do - expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'get /activitypub/success', { 'Date' => 'Wed, 18 Dec 2023 10:00:00 GMT', 'Host' => 'www.example.com' }) - - get '/activitypub/success', headers: { - 'Host' => 'www.example.com', - 'Date' => 'Wed, 18 Dec 2023 10:00:00 GMT', - 'Signature' => signature_header, - } - - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: nil, - error: 'Signed request date outside acceptable time window' - ) - end - end - - context 'with a valid signature on a POST request' do - let(:digest_header) { 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' } - let(:signature_header) do - 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="' # rubocop:disable Layout/LineLength - end - - it 'successfuly verifies signature', :aggregate_failures do - expect(digest_header).to eq digest_value('Hello world') - expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'post /activitypub/success', { 'Host' => 'www.example.com', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Digest' => digest_header }) - - post '/activitypub/success', params: 'Hello world', headers: { - 'Host' => 'www.example.com', - 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', - 'Digest' => digest_header, - 'Signature' => signature_header, - } - - expect(response).to have_http_status(200) - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: actor.id.to_s - ) - end - end - - context 'when the Digest of a POST request is not signed' do - let(:digest_header) { 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' } - let(:signature_header) do - 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date (request-target)",signature="CPD704CG8aCm8X8qIP8kkkiGp1qwFLk/wMVQHOGP0Txxan8c2DZtg/KK7eN8RG8tHx8br/yS2hJs51x4kXImYukGzNJd7ihE3T8lp+9RI1tCcdobTzr/VcVJHDFySdQkg266GCMijRQRZfNvqlJLiisr817PI+gNVBI5qV+vnVd1XhWCEZ+YSmMe8UqYARXAYNqMykTheojqGpTeTFGPUpTQA2Fmt2BipwIjcFDm2Hpihl2kB0MUS0x3zPmHDuadvzoBbN6m3usPDLgYrpALlh+wDs1dYMntcwdwawRKY1oE1XNtgOSum12wntDq3uYL4gya2iPdcw3c929b4koUzw=="' # rubocop:disable Layout/LineLength - end - - it 'fails to verify signature', :aggregate_failures do - expect(digest_header).to eq digest_value('Hello world') - expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'post /activitypub/success', { 'Host' => 'www.example.com', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT' }) - - post '/activitypub/success', params: 'Hello world', headers: { - 'Host' => 'www.example.com', - 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', - 'Digest' => digest_header, - 'Signature' => signature_header, - } - - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: nil, - error: 'Mastodon requires the Digest header to be signed when doing a POST request' - ) - end - end - - context 'with a tampered body on a POST request' do - let(:digest_header) { 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' } - let(:signature_header) do - 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="' # rubocop:disable Layout/LineLength - end - - it 'fails to verify signature', :aggregate_failures do - expect(digest_header).to_not eq digest_value('Hello world!') - expect(signature_header).to eq build_signature_string(actor_keypair, 'https://remote.domain/users/bob#main-key', 'post /activitypub/success', { 'Host' => 'www.example.com', 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', 'Digest' => digest_header }) - - post '/activitypub/success', params: 'Hello world!', headers: { - 'Host' => 'www.example.com', - 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', - 'Digest' => 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=', - 'Signature' => signature_header, - } - - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: nil, - error: 'Invalid Digest value. Computed SHA-256 digest: wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro=; given: ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=' - ) - end - end - - context 'with a tampered path in a POST request' do - it 'fails to verify signature', :aggregate_failures do - post '/activitypub/alternative-path', params: 'Hello world', headers: { - 'Host' => 'www.example.com', - 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', - 'Digest' => 'SHA-256=ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw=', - 'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="', # rubocop:disable Layout/LineLength - } - - expect(response).to have_http_status(200) - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: nil, - error: anything - ) - end - end - end - - context 'with an inaccessible key' do - before do - stub_request(:get, 'https://remote.domain/users/alice#main-key').to_return(status: 404) - end - - it 'fails to verify signature', :aggregate_failures do - get '/activitypub/success', headers: { - 'Host' => 'www.example.com', - 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', - 'Signature' => 'keyId="https://remote.domain/users/alice#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength - } - - expect(body_as_json).to match( - signed_request: true, - signature_actor_id: nil, - error: 'Unable to fetch key JSON at https://remote.domain/users/alice#main-key' - ) - end - end - - private - - def stub_tests_controller - stub_const('ActivityPub::TestsController', activitypub_tests_controller) - - Rails.application.routes.draw do - # NOTE: RouteSet#draw removes all routes, so we need to re-insert one - resource :instance_actor, path: 'actor', only: [:show] - - match :via => [:get, :post], '/activitypub/success' => 'activitypub/tests#success' - match :via => [:get, :post], '/activitypub/alternative-path' => 'activitypub/tests#alternative_success' - match :via => [:get, :post], '/activitypub/signature_required' => 'activitypub/tests#signature_required' - end - end - - def activitypub_tests_controller - Class.new(ApplicationController) do - include SignatureVerification - - before_action :require_actor_signature!, only: [:signature_required] - - def success - render json: { - signed_request: signed_request?, - signature_actor_id: signed_request_actor&.id&.to_s, - }.merge(signature_verification_failure_reason || {}) - end - - alias_method :alternative_success, :success - alias_method :signature_required, :success - end - end - - def digest_value(body) - "SHA-256=#{Digest::SHA256.base64digest(body)}" - end - - def build_signature_string(keypair, key_id, request_target, headers) - algorithm = 'rsa-sha256' - signed_headers = headers.merge({ '(request-target)' => request_target }) - signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") - signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) - - "keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\"" - end -end diff --git a/spec/requests/well_known/change_password_spec.rb b/spec/requests/well_known/change_password_spec.rb deleted file mode 100644 index 04134b71ff..0000000000 --- a/spec/requests/well_known/change_password_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'The /.well-known/change-password request' do - it 'redirects to the change password page' do - get '/.well-known/change-password' - - expect(response).to redirect_to '/auth/edit' - end -end diff --git a/spec/requests/well_known/host_meta_spec.rb b/spec/requests/well_known/host_meta_spec.rb deleted file mode 100644 index ca10a51a01..0000000000 --- a/spec/requests/well_known/host_meta_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'The /.well-known/host-meta request' do - it 'returns http success with valid XML response' do - get '/.well-known/host-meta' - - expect(response) - .to have_http_status(200) - .and have_attributes( - media_type: 'application/xrd+xml', - body: host_meta_xml_template - ) - end - - private - - def host_meta_xml_template - <<~XML - - - - - XML - end -end diff --git a/spec/requests/well_known/node_info_spec.rb b/spec/requests/well_known/node_info_spec.rb deleted file mode 100644 index 0934b0fde6..0000000000 --- a/spec/requests/well_known/node_info_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'The well-known node-info endpoints' do - describe 'The /.well-known/node-info endpoint' do - it 'returns JSON document pointing to node info' do - get '/.well-known/nodeinfo' - - expect(response) - .to have_http_status(200) - .and have_attributes( - media_type: 'application/json' - ) - - expect(body_as_json).to include( - links: be_an(Array).and( - contain_exactly( - include( - rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', - href: include('nodeinfo/2.0') - ) - ) - ) - ) - end - end - - describe 'The /nodeinfo/2.0 endpoint' do - it 'returns JSON document with node info properties' do - get '/nodeinfo/2.0' - - expect(response) - .to have_http_status(200) - .and have_attributes( - media_type: 'application/json' - ) - - expect(non_matching_hash) - .to_not match_json_schema('nodeinfo_2.0') - - expect(body_as_json) - .to match_json_schema('nodeinfo_2.0') - .and include( - version: '2.0', - usage: be_a(Hash), - software: be_a(Hash), - protocols: be_a(Array) - ) - end - - private - - def non_matching_hash - { 'foo' => 0 } - end - end -end diff --git a/spec/requests/well_known/oauth_metadata_spec.rb b/spec/requests/well_known/oauth_metadata_spec.rb deleted file mode 100644 index 9d2d202286..0000000000 --- a/spec/requests/well_known/oauth_metadata_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'The /.well-known/oauth-authorization-server request' do - let(:protocol) { ENV.fetch('LOCAL_HTTPS', true) ? :https : :http } - - before do - host! Rails.configuration.x.local_domain - end - - it 'returns http success with valid JSON response' do - get '/.well-known/oauth-authorization-server' - - expect(response) - .to have_http_status(200) - .and have_attributes( - media_type: 'application/json' - ) - - grant_types_supported = Doorkeeper.configuration.grant_flows.dup - grant_types_supported << 'refresh_token' if Doorkeeper.configuration.refresh_token_enabled? - - expect(body_as_json).to include( - issuer: root_url(protocol: protocol), - service_documentation: 'https://docs.joinmastodon.org/', - authorization_endpoint: oauth_authorization_url(protocol: protocol), - token_endpoint: oauth_token_url(protocol: protocol), - revocation_endpoint: oauth_revoke_url(protocol: protocol), - scopes_supported: Doorkeeper.configuration.scopes.map(&:to_s), - response_types_supported: Doorkeeper.configuration.authorization_response_types, - response_modes_supported: Doorkeeper.configuration.authorization_response_flows.flat_map(&:response_mode_matches).uniq, - token_endpoint_auth_methods_supported: %w(client_secret_basic client_secret_post), - grant_types_supported: grant_types_supported, - code_challenge_methods_supported: ['S256'], - # non-standard extension: - app_registration_endpoint: api_v1_apps_url(protocol: protocol) - ) - end -end diff --git a/spec/requests/well_known/webfinger_spec.rb b/spec/requests/well_known/webfinger_spec.rb deleted file mode 100644 index 0aafdf5624..0000000000 --- a/spec/requests/well_known/webfinger_spec.rb +++ /dev/null @@ -1,251 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'The /.well-known/webfinger endpoint' do - subject(:perform_request!) { get webfinger_url(resource: resource) } - - let(:alternate_domains) { [] } - let(:alice) { Fabricate(:account, username: 'alice') } - let(:resource) { nil } - - around do |example| - tmp = Rails.configuration.x.alternate_domains - Rails.configuration.x.alternate_domains = alternate_domains - example.run - Rails.configuration.x.alternate_domains = tmp - end - - shared_examples 'a successful response' do - it 'returns http success with correct media type and headers and body json' do - expect(response).to have_http_status(200) - - expect(response.headers['Vary']).to eq('Origin') - - expect(response.media_type).to eq 'application/jrd+json' - - expect(body_as_json) - .to include( - subject: eq('acct:alice@cb6e6126.ngrok.io'), - aliases: include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice') - ) - end - end - - context 'when an account exists' do - let(:resource) { alice.to_webfinger_s } - - before do - perform_request! - end - - it_behaves_like 'a successful response' - end - - context 'when an account is temporarily suspended' do - let(:resource) { alice.to_webfinger_s } - - before do - alice.suspend! - perform_request! - end - - it_behaves_like 'a successful response' - end - - context 'when an account is permanently suspended or deleted' do - let(:resource) { alice.to_webfinger_s } - - before do - alice.suspend! - alice.deletion_request.destroy - perform_request! - end - - it 'returns http gone' do - expect(response).to have_http_status(410) - end - end - - context 'when an account is not found' do - let(:resource) { 'acct:not@existing.com' } - - before do - perform_request! - end - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - - context 'with an alternate domain' do - let(:alternate_domains) { ['foo.org'] } - - before do - perform_request! - end - - context 'when an account exists' do - let(:resource) do - username, = alice.to_webfinger_s.split('@') - "#{username}@foo.org" - end - - it_behaves_like 'a successful response' - end - - context 'when the domain is wrong' do - let(:resource) do - username, = alice.to_webfinger_s.split('@') - "#{username}@bar.org" - end - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end - - context 'when the old name scheme is used to query the instance actor' do - let(:resource) do - "#{Rails.configuration.x.local_domain}@#{Rails.configuration.x.local_domain}" - end - - before do - perform_request! - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'sets only a Vary Origin header' do - expect(response.headers['Vary']).to eq('Origin') - end - - it 'returns application/jrd+json' do - expect(response.media_type).to eq 'application/jrd+json' - end - - it 'returns links for the internal account' do - json = body_as_json - expect(json[:subject]).to eq 'acct:mastodon.internal@cb6e6126.ngrok.io' - expect(json[:aliases]).to eq ['https://cb6e6126.ngrok.io/actor'] - end - end - - context 'with no resource parameter' do - let(:resource) { nil } - - before do - perform_request! - end - - it 'returns http bad request' do - expect(response).to have_http_status(400) - end - end - - context 'with a nonsense parameter' do - let(:resource) { 'df/:dfkj' } - - before do - perform_request! - end - - it 'returns http bad request' do - expect(response).to have_http_status(400) - end - end - - context 'when an account has an avatar' do - let(:alice) { Fabricate(:account, username: 'alice', avatar: attachment_fixture('attachment.jpg')) } - let(:resource) { alice.to_webfinger_s } - - it 'returns avatar in response' do - perform_request! - - avatar_link = get_avatar_link(body_as_json) - expect(avatar_link).to_not be_nil - expect(avatar_link[:type]).to eq alice.avatar.content_type - expect(avatar_link[:href]).to eq Addressable::URI.new(host: Rails.configuration.x.local_domain, path: alice.avatar.to_s, scheme: 'https').to_s - end - - context 'with limited federation mode' do - before do - allow(Rails.configuration.x).to receive(:limited_federation_mode).and_return(true) - end - - it 'does not return avatar in response' do - perform_request! - - avatar_link = get_avatar_link(body_as_json) - expect(avatar_link).to be_nil - end - end - - context 'when enabling DISALLOW_UNAUTHENTICATED_API_ACCESS' do - around do |example| - ClimateControl.modify DISALLOW_UNAUTHENTICATED_API_ACCESS: 'true' do - example.run - end - end - - it 'does not return avatar in response' do - perform_request! - - avatar_link = get_avatar_link(body_as_json) - expect(avatar_link).to be_nil - end - end - end - - context 'when an account does not have an avatar' do - let(:alice) { Fabricate(:account, username: 'alice', avatar: nil) } - let(:resource) { alice.to_webfinger_s } - - before do - perform_request! - end - - it 'does not return avatar in response' do - avatar_link = get_avatar_link(body_as_json) - expect(avatar_link).to be_nil - end - end - - context 'with different headers' do - describe 'requested with standard accepts headers' do - it 'returns a json response' do - get webfinger_url(resource: alice.to_webfinger_s) - - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/jrd+json' - end - end - - describe 'asking for json format' do - it 'returns a json response for json format' do - get webfinger_url(resource: alice.to_webfinger_s, format: :json) - - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/jrd+json' - end - - it 'returns a json response for json accept header' do - headers = { 'HTTP_ACCEPT' => 'application/jrd+json' } - get webfinger_url(resource: alice.to_webfinger_s), headers: headers - - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/jrd+json' - end - end - end - - private - - def get_avatar_link(json) - json[:links].find { |link| link[:rel] == 'http://webfinger.net/rel/avatar' } - end -end diff --git a/spec/routing/accounts_routing_spec.rb b/spec/routing/accounts_routing_spec.rb deleted file mode 100644 index 588855943e..0000000000 --- a/spec/routing/accounts_routing_spec.rb +++ /dev/null @@ -1,186 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Routes under accounts/' do - context 'with local username' do - let(:username) { 'alice' } - - it 'routes /@:username' do - expect(get("/@#{username}")).to route_to('accounts#show', username: username) - end - - it 'routes /@:username.json' do - expect(get("/@#{username}.json")).to route_to('accounts#show', username: username, format: 'json') - end - - it 'routes /@:username.rss' do - expect(get("/@#{username}.rss")).to route_to('accounts#show', username: username, format: 'rss') - end - - it 'routes /@:username/:id' do - expect(get("/@#{username}/123")).to route_to('statuses#show', account_username: username, id: '123') - end - - it 'routes /@:username/:id/embed' do - expect(get("/@#{username}/123/embed")).to route_to('statuses#embed', account_username: username, id: '123') - end - - it 'routes /@:username/following' do - expect(get("/@#{username}/following")).to route_to('following_accounts#index', account_username: username) - end - - it 'routes /@:username/followers' do - expect(get("/@#{username}/followers")).to route_to('follower_accounts#index', account_username: username) - end - - it 'routes /@:username/with_replies' do - expect(get("/@#{username}/with_replies")).to route_to('accounts#show', username: username) - end - - it 'routes /@:username/media' do - expect(get("/@#{username}/media")).to route_to('accounts#show', username: username) - end - - it 'routes /@:username/tagged/:tag' do - expect(get("/@#{username}/tagged/foo")).to route_to('accounts#show', username: username, tag: 'foo') - end - end - - context 'with local username encoded at' do - include RSpec::Rails::RequestExampleGroup - let(:username) { 'alice' } - - it 'routes /%40:username' do - get "/%40#{username}" - expect(response).to redirect_to("/@#{username}") - end - - it 'routes /%40:username.json' do - get("/%40#{username}.json") - expect(response).to redirect_to("/@#{username}.json") - end - - it 'routes /%40:username.rss' do - get("/%40#{username}.rss") - expect(response).to redirect_to("/@#{username}.rss") - end - - it 'routes /%40:username/:id' do - get("/%40#{username}/123") - expect(response).to redirect_to("/@#{username}/123") - end - - it 'routes /%40:username/:id/embed' do - get("/%40#{username}/123/embed") - expect(response).to redirect_to("/@#{username}/123/embed") - end - - it 'routes /%40:username/following' do - get("/%40#{username}/following") - expect(response).to redirect_to("/@#{username}/following") - end - - it 'routes /%40:username/followers' do - get("/%40#{username}/followers") - expect(response).to redirect_to("/@#{username}/followers") - end - - it 'routes /%40:username/with_replies' do - get("/%40#{username}/with_replies") - expect(response).to redirect_to("/@#{username}/with_replies") - end - - it 'routes /%40:username/media' do - get("/%40#{username}/media") - expect(response).to redirect_to("/@#{username}/media") - end - - it 'routes /%40:username/tagged/:tag' do - get("/%40#{username}/tagged/foo") - expect(response).to redirect_to("/@#{username}/tagged/foo") - end - end - - context 'with remote username' do - let(:username) { 'alice@example.com' } - - it 'routes /@:username' do - expect(get("/@#{username}")).to route_to('home#index', username_with_domain: username) - end - - it 'routes /@:username/:id' do - expect(get("/@#{username}/123")).to route_to('home#index', username_with_domain: username, any: '123') - end - - it 'routes /@:username/:id/embed' do - expect(get("/@#{username}/123/embed")).to route_to('home#index', username_with_domain: username, any: '123/embed') - end - - it 'routes /@:username/following' do - expect(get("/@#{username}/following")).to route_to('home#index', username_with_domain: username, any: 'following') - end - - it 'routes /@:username/followers' do - expect(get("/@#{username}/followers")).to route_to('home#index', username_with_domain: username, any: 'followers') - end - - it 'routes /@:username/with_replies' do - expect(get("/@#{username}/with_replies")).to route_to('home#index', username_with_domain: username, any: 'with_replies') - end - - it 'routes /@:username/media' do - expect(get("/@#{username}/media")).to route_to('home#index', username_with_domain: username, any: 'media') - end - - it 'routes /@:username/tagged/:tag' do - expect(get("/@#{username}/tagged/foo")).to route_to('home#index', username_with_domain: username, any: 'tagged/foo') - end - end - - context 'with remote username encoded at' do - include RSpec::Rails::RequestExampleGroup - let(:username) { 'alice%40example.com' } - let(:username_decoded) { 'alice@example.com' } - - it 'routes /%40:username' do - get("/%40#{username}") - expect(response).to redirect_to("/@#{username_decoded}") - end - - it 'routes /%40:username/:id' do - get("/%40#{username}/123") - expect(response).to redirect_to("/@#{username_decoded}/123") - end - - it 'routes /%40:username/:id/embed' do - get("/%40#{username}/123/embed") - expect(response).to redirect_to("/@#{username_decoded}/123/embed") - end - - it 'routes /%40:username/following' do - get("/%40#{username}/following") - expect(response).to redirect_to("/@#{username_decoded}/following") - end - - it 'routes /%40:username/followers' do - get("/%40#{username}/followers") - expect(response).to redirect_to("/@#{username_decoded}/followers") - end - - it 'routes /%40:username/with_replies' do - get("/%40#{username}/with_replies") - expect(response).to redirect_to("/@#{username_decoded}/with_replies") - end - - it 'routes /%40:username/media' do - get("/%40#{username}/media") - expect(response).to redirect_to("/@#{username_decoded}/media") - end - - it 'routes /%40:username/tagged/:tag' do - get("/%40#{username}/tagged/foo") - expect(response).to redirect_to("/@#{username_decoded}/tagged/foo") - end - end -end diff --git a/spec/routing/api_routing_spec.rb b/spec/routing/api_routing_spec.rb deleted file mode 100644 index a822fba4c5..0000000000 --- a/spec/routing/api_routing_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'API routes' do - describe 'Credentials routes' do - it 'routes to verify credentials' do - expect(get('/api/v1/accounts/verify_credentials')) - .to route_to('api/v1/accounts/credentials#show') - end - - it 'routes to update credentials' do - expect(patch('/api/v1/accounts/update_credentials')) - .to route_to('api/v1/accounts/credentials#update') - end - end - - describe 'Account routes' do - it 'routes to statuses' do - expect(get('/api/v1/accounts/user/statuses')) - .to route_to('api/v1/accounts/statuses#index', account_id: 'user') - end - - it 'routes to followers' do - expect(get('/api/v1/accounts/user/followers')) - .to route_to('api/v1/accounts/follower_accounts#index', account_id: 'user') - end - - it 'routes to following' do - expect(get('/api/v1/accounts/user/following')) - .to route_to('api/v1/accounts/following_accounts#index', account_id: 'user') - end - - it 'routes to search' do - expect(get('/api/v1/accounts/search')) - .to route_to('api/v1/accounts/search#show') - end - - it 'routes to relationships' do - expect(get('/api/v1/accounts/relationships')) - .to route_to('api/v1/accounts/relationships#index') - end - end - - describe 'Statuses routes' do - it 'routes reblogged_by' do - expect(get('/api/v1/statuses/123/reblogged_by')) - .to route_to('api/v1/statuses/reblogged_by_accounts#index', status_id: '123') - end - - it 'routes favourited_by' do - expect(get('/api/v1/statuses/123/favourited_by')) - .to route_to('api/v1/statuses/favourited_by_accounts#index', status_id: '123') - end - - it 'routes reblog' do - expect(post('/api/v1/statuses/123/reblog')) - .to route_to('api/v1/statuses/reblogs#create', status_id: '123') - end - - it 'routes unreblog' do - expect(post('/api/v1/statuses/123/unreblog')) - .to route_to('api/v1/statuses/reblogs#destroy', status_id: '123') - end - - it 'routes favourite' do - expect(post('/api/v1/statuses/123/favourite')) - .to route_to('api/v1/statuses/favourites#create', status_id: '123') - end - - it 'routes unfavourite' do - expect(post('/api/v1/statuses/123/unfavourite')) - .to route_to('api/v1/statuses/favourites#destroy', status_id: '123') - end - - it 'routes mute' do - expect(post('/api/v1/statuses/123/mute')) - .to route_to('api/v1/statuses/mutes#create', status_id: '123') - end - - it 'routes unmute' do - expect(post('/api/v1/statuses/123/unmute')) - .to route_to('api/v1/statuses/mutes#destroy', status_id: '123') - end - end - - describe 'Timeline routes' do - it 'routes to home timeline' do - expect(get('/api/v1/timelines/home')) - .to route_to('api/v1/timelines/home#show') - end - - it 'routes to public timeline' do - expect(get('/api/v1/timelines/public')) - .to route_to('api/v1/timelines/public#show') - end - - it 'routes to tag timeline' do - expect(get('/api/v1/timelines/tag/test')) - .to route_to('api/v1/timelines/tag#show', id: 'test') - end - end -end diff --git a/spec/routing/well_known_routes_spec.rb b/spec/routing/well_known_routes_spec.rb deleted file mode 100644 index 8cf08c13c1..0000000000 --- a/spec/routing/well_known_routes_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Well Known routes' do - describe 'the host-meta route' do - it 'routes to correct place with xml format' do - expect(get('/.well-known/host-meta')) - .to route_to('well_known/host_meta#show', format: 'xml') - end - end - - describe 'the webfinger route' do - it 'routes to correct place with json format' do - expect(get('/.well-known/webfinger')) - .to route_to('well_known/webfinger#show') - end - end -end diff --git a/spec/search/models/concerns/account/search_spec.rb b/spec/search/models/concerns/account/search_spec.rb deleted file mode 100644 index d8d7f355dd..0000000000 --- a/spec/search/models/concerns/account/search_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Account::Search do - describe 'a non-discoverable account becoming discoverable' do - let(:account) { Account.find_by(username: 'search_test_account_1') } - - context 'when picking a non-discoverable account' do - it 'its bio is not in the AccountsIndex' do - results = AccountsIndex.filter(term: { username: account.username }) - expect(results.count).to eq(1) - expect(results.first.text).to be_nil - end - end - - context 'when the non-discoverable account becomes discoverable' do - it 'its bio is added to the AccountsIndex' do - account.discoverable = true - account.save! - - results = AccountsIndex.filter(term: { username: account.username }) - expect(results.count).to eq(1) - expect(results.first.text).to eq(account.note) - end - end - end - - describe 'a discoverable account becoming non-discoverable' do - let(:account) { Account.find_by(username: 'search_test_account_0') } - - context 'when picking an discoverable account' do - it 'has its bio in the AccountsIndex' do - results = AccountsIndex.filter(term: { username: account.username }) - expect(results.count).to eq(1) - expect(results.first.text).to eq(account.note) - end - end - - context 'when the discoverable account becomes non-discoverable' do - it 'its bio is removed from the AccountsIndex' do - account.discoverable = false - account.save! - - results = AccountsIndex.filter(term: { username: account.username }) - expect(results.count).to eq(1) - expect(results.first.text).to be_nil - end - end - end -end diff --git a/spec/search/models/concerns/account/statuses_search_spec.rb b/spec/search/models/concerns/account/statuses_search_spec.rb deleted file mode 100644 index b1bf4968ca..0000000000 --- a/spec/search/models/concerns/account/statuses_search_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Account::StatusesSearch, :inline_jobs do - describe 'a non-indexable account becoming indexable' do - let(:account) { Account.find_by(username: 'search_test_account_1') } - - context 'when picking a non-indexable account' do - it 'has no statuses in the PublicStatusesIndex' do - expect(PublicStatusesIndex.filter(term: { account_id: account.id }).count).to eq(0) - end - - it 'has statuses in the StatusesIndex' do - expect(StatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.count) - end - end - - context 'when the non-indexable account becomes indexable' do - it 'adds the public statuses to the PublicStatusesIndex' do - account.indexable = true - account.save! - - expect(PublicStatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.public_visibility.count) - expect(StatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.count) - end - end - end - - describe 'an indexable account becoming non-indexable' do - let(:account) { Account.find_by(username: 'search_test_account_0') } - - context 'when picking an indexable account' do - it 'has statuses in the PublicStatusesIndex' do - expect(PublicStatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.public_visibility.count) - end - - it 'has statuses in the StatusesIndex' do - expect(StatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.count) - end - end - - context 'when the indexable account becomes non-indexable' do - it 'removes the statuses from the PublicStatusesIndex' do - account.indexable = false - account.save! - - expect(PublicStatusesIndex.filter(term: { account_id: account.id }).count).to eq(0) - expect(StatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.count) - end - end - end -end diff --git a/spec/serializers/activitypub/device_serializer_spec.rb b/spec/serializers/activitypub/device_serializer_spec.rb deleted file mode 100644 index 23f0b24c4e..0000000000 --- a/spec/serializers/activitypub/device_serializer_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe ActivityPub::DeviceSerializer do - let(:serialization) { serialized_record_json(record, described_class) } - let(:record) { Fabricate(:device) } - - describe 'type' do - it 'returns correct serialized type' do - expect(serialization['type']).to eq('Device') - end - end -end diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb deleted file mode 100644 index 338d66b308..0000000000 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe ActivityPub::NoteSerializer do - subject { serialized_record_json(parent, described_class, adapter: ActivityPub::Adapter) } - - let!(:account) { Fabricate(:account) } - let!(:other) { Fabricate(:account) } - let!(:parent) { Fabricate(:status, account: account, visibility: :public, language: 'zh-TW') } - let!(:reply_by_account_first) { Fabricate(:status, account: account, thread: parent, visibility: :public) } - let!(:reply_by_account_next) { Fabricate(:status, account: account, thread: parent, visibility: :public) } - let!(:reply_by_other_first) { Fabricate(:status, account: other, thread: parent, visibility: :public) } - let!(:reply_by_account_third) { Fabricate(:status, account: account, thread: parent, visibility: :public) } - let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) } - - it 'has the expected shape' do - expect(subject).to include({ - '@context' => include('https://www.w3.org/ns/activitystreams'), - 'type' => 'Note', - 'attributedTo' => ActivityPub::TagManager.instance.uri_for(account), - 'contentMap' => include({ - 'zh-TW' => a_kind_of(String), - }), - }) - end - - it 'has a replies collection' do - expect(subject['replies']['type']).to eql('Collection') - end - - it 'has a replies collection with a first Page' do - expect(subject['replies']['first']['type']).to eql('CollectionPage') - end - - it 'includes public self-replies in its replies collection' do - expect(subject['replies']['first']['items']).to include(reply_by_account_first.uri, reply_by_account_next.uri, reply_by_account_third.uri) - end - - it 'does not include replies from others in its replies collection' do - expect(subject['replies']['first']['items']).to_not include(reply_by_other_first.uri) - end - - it 'does not include replies with direct visibility in its replies collection' do - expect(subject['replies']['first']['items']).to_not include(reply_by_account_visibility_direct.uri) - end -end diff --git a/spec/serializers/activitypub/one_time_key_serializer_spec.rb b/spec/serializers/activitypub/one_time_key_serializer_spec.rb deleted file mode 100644 index 89efe95c8c..0000000000 --- a/spec/serializers/activitypub/one_time_key_serializer_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe ActivityPub::OneTimeKeySerializer do - let(:serialization) { serialized_record_json(record, described_class) } - let(:record) { Fabricate(:one_time_key) } - - describe 'type' do - it 'returns correct serialized type' do - expect(serialization['type']).to eq('Curve25519Key') - end - end -end diff --git a/spec/serializers/activitypub/undo_like_serializer_spec.rb b/spec/serializers/activitypub/undo_like_serializer_spec.rb deleted file mode 100644 index 3d61e86751..0000000000 --- a/spec/serializers/activitypub/undo_like_serializer_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe ActivityPub::UndoLikeSerializer do - let(:serialization) { serialized_record_json(record, described_class) } - let(:record) { Fabricate(:favourite) } - - describe 'type' do - it 'returns correct serialized type' do - expect(serialization['type']).to eq('Undo') - end - end -end diff --git a/spec/serializers/activitypub/update_poll_serializer_spec.rb b/spec/serializers/activitypub/update_poll_serializer_spec.rb deleted file mode 100644 index 8ff4fd2701..0000000000 --- a/spec/serializers/activitypub/update_poll_serializer_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe ActivityPub::UpdatePollSerializer do - subject { serialized_record_json(status, described_class, adapter: ActivityPub::Adapter) } - - let(:account) { Fabricate(:account) } - let(:poll) { Fabricate(:poll, account: account) } - let!(:status) { Fabricate(:status, account: account, poll: poll) } - - it 'has a Update type' do - expect(subject['type']).to eql('Update') - end - - it 'has an object with Question type' do - expect(subject['object']['type']).to eql('Question') - end - - it 'has the correct actor URI set' do - expect(subject['actor']).to eql(ActivityPub::TagManager.instance.uri_for(account)) - end -end diff --git a/spec/serializers/activitypub/vote_serializer_spec.rb b/spec/serializers/activitypub/vote_serializer_spec.rb deleted file mode 100644 index b7c0b8928b..0000000000 --- a/spec/serializers/activitypub/vote_serializer_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe ActivityPub::VoteSerializer do - let(:serialization) { serialized_record_json(record, described_class) } - let(:record) { Fabricate(:poll_vote) } - - describe 'type' do - it 'returns correct serialized type' do - expect(serialization['type']).to eq('Create') - end - end -end diff --git a/spec/serializers/rest/account_serializer_spec.rb b/spec/serializers/rest/account_serializer_spec.rb deleted file mode 100644 index 15939e484d..0000000000 --- a/spec/serializers/rest/account_serializer_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe REST::AccountSerializer do - subject { serialized_record_json(account, described_class) } - - let(:role) { Fabricate(:user_role, name: 'Role', highlighted: true) } - let(:user) { Fabricate(:user, role: role) } - let(:account) { user.account } - - context 'when the account is suspended' do - before do - account.suspend! - end - - it 'returns empty roles' do - expect(subject['roles']).to eq [] - end - end - - context 'when the account has a highlighted role' do - let(:role) { Fabricate(:user_role, name: 'Role', highlighted: true) } - - it 'returns the expected role' do - expect(subject['roles'].first).to include({ 'name' => 'Role' }) - end - end - - context 'when the account has a non-highlighted role' do - let(:role) { Fabricate(:user_role, name: 'Role', highlighted: false) } - - it 'returns empty roles' do - expect(subject['roles']).to eq [] - end - end - - context 'when the account is memorialized' do - before do - account.memorialize! - end - - it 'marks it as such' do - expect(subject['memorial']).to be true - end - end -end diff --git a/spec/serializers/rest/encrypted_message_serializer_spec.rb b/spec/serializers/rest/encrypted_message_serializer_spec.rb deleted file mode 100644 index 01db1149af..0000000000 --- a/spec/serializers/rest/encrypted_message_serializer_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe REST::EncryptedMessageSerializer do - let(:serialization) { serialized_record_json(record, described_class) } - let(:record) { Fabricate(:encrypted_message) } - - describe 'account' do - it 'returns the associated account' do - expect(serialization['account_id']).to eq(record.from_account.id.to_s) - end - end -end diff --git a/spec/serializers/rest/instance_serializer_spec.rb b/spec/serializers/rest/instance_serializer_spec.rb deleted file mode 100644 index 39e6b3820b..0000000000 --- a/spec/serializers/rest/instance_serializer_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe REST::InstanceSerializer do - let(:serialization) { serialized_record_json(record, described_class) } - let(:record) { InstancePresenter.new } - - describe 'usage' do - it 'returns recent usage data' do - expect(serialization['usage']).to eq({ 'users' => { 'active_month' => 0 } }) - end - end - - describe 'configuration' do - it 'returns the VAPID public key' do - expect(serialization['configuration']['vapid']).to eq({ - 'public_key' => Rails.configuration.x.vapid_public_key, - }) - end - - it 'returns the max pinned statuses limit' do - expect(serialization.deep_symbolize_keys) - .to include( - configuration: include( - accounts: include(max_pinned_statuses: StatusPinValidator::PIN_LIMIT) - ) - ) - end - end -end diff --git a/spec/serializers/rest/keys/claim_result_serializer_spec.rb b/spec/serializers/rest/keys/claim_result_serializer_spec.rb deleted file mode 100644 index 7f7fb850cd..0000000000 --- a/spec/serializers/rest/keys/claim_result_serializer_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe REST::Keys::ClaimResultSerializer do - let(:serialization) { serialized_record_json(record, described_class) } - let(:record) { Keys::ClaimService::Result.new(Account.new(id: 123), 456) } - - describe 'account' do - it 'returns the associated account' do - expect(serialization['account_id']).to eq('123') - end - end -end diff --git a/spec/serializers/rest/keys/device_serializer_spec.rb b/spec/serializers/rest/keys/device_serializer_spec.rb deleted file mode 100644 index 28177a3db5..0000000000 --- a/spec/serializers/rest/keys/device_serializer_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe REST::Keys::DeviceSerializer do - let(:serialization) { serialized_record_json(record, described_class) } - let(:record) { Device.new(name: 'Device name') } - - describe 'name' do - it 'returns the name' do - expect(serialization['name']).to eq('Device name') - end - end -end diff --git a/spec/serializers/rest/keys/query_result_serializer_spec.rb b/spec/serializers/rest/keys/query_result_serializer_spec.rb deleted file mode 100644 index ef67d70675..0000000000 --- a/spec/serializers/rest/keys/query_result_serializer_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe REST::Keys::QueryResultSerializer do - let(:serialization) { serialized_record_json(record, described_class) } - let(:record) { Keys::QueryService::Result.new(Account.new(id: 123), []) } - - describe 'account' do - it 'returns the associated account id' do - expect(serialization['account_id']).to eq('123') - end - end -end diff --git a/spec/serializers/rest/suggestion_serializer_spec.rb b/spec/serializers/rest/suggestion_serializer_spec.rb deleted file mode 100644 index b5efba082d..0000000000 --- a/spec/serializers/rest/suggestion_serializer_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe REST::SuggestionSerializer do - let(:serialization) { serialized_record_json(record, described_class) } - let(:record) do - AccountSuggestions::Suggestion.new( - account: account, - sources: ['SuggestionSource'] - ) - end - let(:account) { Fabricate(:account) } - - describe 'account' do - it 'returns the associated account' do - expect(serialization['account']['id']).to eq(account.id.to_s) - end - end -end diff --git a/spec/services/account_search_service_spec.rb b/spec/services/account_search_service_spec.rb deleted file mode 100644 index 5ec0885903..0000000000 --- a/spec/services/account_search_service_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe AccountSearchService do - describe '#call' do - context 'with a query to ignore' do - it 'returns empty array for missing query' do - results = subject.call('', nil, limit: 10) - - expect(results).to eq [] - end - - it 'returns empty array for limit zero' do - Fabricate(:account, username: 'match') - - results = subject.call('match', nil, limit: 0) - - expect(results).to eq [] - end - end - - context 'when searching for a simple term that is not an exact match' do - it 'does not return a nil entry in the array for the exact match' do - account = Fabricate(:account, username: 'matchingusername') - results = subject.call('match', nil, limit: 5) - - expect(results).to eq [account] - end - end - - context 'when there is a local domain' do - around do |example| - before = Rails.configuration.x.local_domain - - example.run - - Rails.configuration.x.local_domain = before - end - - it 'returns exact match first' do - remote = Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e') - remote_too = Fabricate(:account, username: 'b', domain: 'remote', display_name: 'e') - exact = Fabricate(:account, username: 'e') - - Rails.configuration.x.local_domain = 'example.com' - - results = subject.call('e@example.com', nil, limit: 2) - - expect(results).to eq([exact, remote]).or eq([exact, remote_too]) - end - end - - context 'when there is a domain but no exact match' do - it 'follows the remote account when resolve is true' do - service = instance_double(ResolveAccountService, call: nil) - allow(ResolveAccountService).to receive(:new).and_return(service) - - subject.call('newuser@remote.com', nil, limit: 10, resolve: true) - expect(service).to have_received(:call).with('newuser@remote.com') - end - - it 'does not follow the remote account when resolve is false' do - service = instance_double(ResolveAccountService, call: nil) - allow(ResolveAccountService).to receive(:new).and_return(service) - - subject.call('newuser@remote.com', nil, limit: 10, resolve: false) - expect(service).to_not have_received(:call) - end - end - - it 'returns the fuzzy match first, and does not return suspended exacts' do - partial = Fabricate(:account, username: 'exactness') - Fabricate(:account, username: 'exact', suspended: true) - results = subject.call('exact', nil, limit: 10) - - expect(results.size).to eq 1 - expect(results).to eq [partial] - end - - it 'does not return suspended remote accounts' do - Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e', suspended: true) - results = subject.call('a@example.com', nil, limit: 2) - - expect(results.size).to eq 0 - expect(results).to eq [] - end - end -end diff --git a/spec/services/account_statuses_cleanup_service_spec.rb b/spec/services/account_statuses_cleanup_service_spec.rb deleted file mode 100644 index 403c4632d7..0000000000 --- a/spec/services/account_statuses_cleanup_service_spec.rb +++ /dev/null @@ -1,110 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe AccountStatusesCleanupService do - let(:account) { Fabricate(:account, username: 'alice', domain: nil) } - let(:account_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } - let!(:unrelated_status) { Fabricate(:status, created_at: 3.years.ago) } - - describe '#call' do - context 'when the account has not posted anything' do - it 'returns 0 deleted toots' do - expect(subject.call(account_policy)).to eq 0 - end - end - - context 'when the account has posted several old statuses' do - let!(:very_old_status) { Fabricate(:status, created_at: 3.years.ago, account: account) } - let!(:old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } - let!(:another_old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } - let!(:recent_status) { Fabricate(:status, created_at: 1.day.ago, account: account) } - - context 'when given a budget of 1' do - it 'reports 1 deleted toot' do - expect(subject.call(account_policy, 1)).to eq 1 - end - end - - context 'when given a normal budget of 10' do - it 'reports 3 deleted statuses' do - expect(subject.call(account_policy, 10)).to eq 3 - end - - it 'records the last deleted id' do - subject.call(account_policy, 10) - expect(account_policy.last_inspected).to eq [old_status.id, another_old_status.id].max - end - - it 'actually deletes the statuses' do - subject.call(account_policy, 10) - expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])).to be_nil - expect { recent_status.reload }.to_not raise_error - end - - it 'preserves recent and unrelated statuses' do - subject.call(account_policy, 10) - expect { unrelated_status.reload }.to_not raise_error - expect { recent_status.reload }.to_not raise_error - end - end - - context 'when called repeatedly with a budget of 2' do - it 'reports 2 then 1 deleted statuses' do - expect(subject.call(account_policy, 2)).to eq 2 - expect(subject.call(account_policy, 2)).to eq 1 - end - - it 'actually deletes the statuses in the expected order' do - subject.call(account_policy, 2) - expect(Status.find_by(id: very_old_status.id)).to be_nil - subject.call(account_policy, 2) - expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])).to be_nil - end - end - - context 'when a self-faved toot is unfaved' do - let!(:self_faved) { Fabricate(:status, created_at: 6.months.ago, account: account) } - let!(:favourite) { Fabricate(:favourite, account: account, status: self_faved) } - - it 'deletes it once unfaved' do - expect(subject.call(account_policy, 20)).to eq 3 - expect(Status.find_by(id: self_faved.id)).to_not be_nil - expect(subject.call(account_policy, 20)).to eq 0 - favourite.destroy! - expect(subject.call(account_policy, 20)).to eq 1 - expect(Status.find_by(id: self_faved.id)).to be_nil - end - end - - context 'when there are more un-deletable old toots than the early search cutoff' do - before do - stub_const 'AccountStatusesCleanupPolicy::EARLY_SEARCH_CUTOFF', 5 - # Old statuses that should be cut-off - 10.times do - Fabricate(:status, created_at: 4.years.ago, visibility: :direct, account: account) - end - # New statuses that prevent cut-off id to reach the last status - 10.times do - Fabricate(:status, created_at: 4.seconds.ago, visibility: :direct, account: account) - end - end - - it 'reports 0 deleted statuses then 0 then 3 then 0 again' do - expect(subject.call(account_policy, 10)).to eq 0 - expect(subject.call(account_policy, 10)).to eq 0 - expect(subject.call(account_policy, 10)).to eq 3 - expect(subject.call(account_policy, 10)).to eq 0 - end - - it 'never causes the recorded id to get higher than oldest deletable toot' do - subject.call(account_policy, 10) - subject.call(account_policy, 10) - subject.call(account_policy, 10) - subject.call(account_policy, 10) - expect(account_policy.last_inspected).to be < Mastodon::Snowflake.id_at(account_policy.min_status_age.seconds.ago, with_random: false) - end - end - end - end -end diff --git a/spec/services/activitypub/fetch_featured_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_collection_service_spec.rb deleted file mode 100644 index 7ea87922ac..0000000000 --- a/spec/services/activitypub/fetch_featured_collection_service_spec.rb +++ /dev/null @@ -1,171 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::FetchFeaturedCollectionService do - subject { described_class.new } - - let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/account', featured_collection_url: 'https://example.com/account/pinned') } - - let!(:known_status) { Fabricate(:status, account: actor, uri: 'https://example.com/account/pinned/1') } - - let(:status_json_pinned_known) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Note', - id: 'https://example.com/account/pinned/known', - content: 'foo', - attributedTo: actor.uri, - to: 'https://www.w3.org/ns/activitystreams#Public', - } - end - - let(:status_json_pinned_unknown_inlined) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Note', - id: 'https://example.com/account/pinned/unknown-inlined', - content: 'foo', - attributedTo: actor.uri, - to: 'https://www.w3.org/ns/activitystreams#Public', - } - end - - let(:status_json_pinned_unknown_reachable) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Note', - id: 'https://example.com/account/pinned/unknown-reachable', - content: 'foo', - attributedTo: actor.uri, - to: 'https://www.w3.org/ns/activitystreams#Public', - } - end - - let(:featured_with_null) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://example.com/account/collections/featured', - totalItems: 0, - type: 'OrderedCollection', - } - end - - let(:items) do - [ - 'https://example.com/account/pinned/known', # known - status_json_pinned_unknown_inlined, # unknown inlined - 'https://example.com/account/pinned/unknown-unreachable', # unknown unreachable - 'https://example.com/account/pinned/unknown-reachable', # unknown reachable - 'https://example.com/account/collections/featured', # featured with null - ] - end - - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Collection', - id: actor.featured_collection_url, - items: items, - }.with_indifferent_access - end - - shared_examples 'sets pinned posts' do - before do - stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404) - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: Oj.dump(featured_with_null), headers: { 'Content-Type': 'application/activity+json' }) - - subject.call(actor, note: true, hashtag: false) - end - - it 'sets expected posts as pinned posts' do - expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly( - 'https://example.com/account/pinned/known', - 'https://example.com/account/pinned/unknown-inlined', - 'https://example.com/account/pinned/unknown-reachable' - ) - expect(actor.pinned_statuses).to_not include(known_status) - end - end - - describe '#call' do - context 'when the endpoint is a Collection' do - before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - end - - it_behaves_like 'sets pinned posts' - end - - context 'when the endpoint is an OrderedCollection' do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'OrderedCollection', - id: actor.featured_collection_url, - orderedItems: items, - }.with_indifferent_access - end - - before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - end - - it_behaves_like 'sets pinned posts' - - context 'when there is a single item, with the array compacted away' do - let(:items) { 'https://example.com/account/pinned/unknown-reachable' } - - before do - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) - subject.call(actor, note: true, hashtag: false) - end - - it 'sets expected posts as pinned posts' do - expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly( - 'https://example.com/account/pinned/unknown-reachable' - ) - end - end - end - - context 'when the endpoint is a paginated Collection' do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Collection', - id: actor.featured_collection_url, - first: { - type: 'CollectionPage', - partOf: actor.featured_collection_url, - items: items, - }, - }.with_indifferent_access - end - - before do - stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - end - - it_behaves_like 'sets pinned posts' - - context 'when there is a single item, with the array compacted away' do - let(:items) { 'https://example.com/account/pinned/unknown-reachable' } - - before do - stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' }) - subject.call(actor, note: true, hashtag: false) - end - - it 'sets expected posts as pinned posts' do - expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly( - 'https://example.com/account/pinned/unknown-reachable' - ) - end - end - end - end -end diff --git a/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb deleted file mode 100644 index 59367b1e32..0000000000 --- a/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService do - subject { described_class.new } - - let(:collection_url) { 'https://example.com/account/tags' } - let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/account') } - - let(:items) do - [ - { type: 'Hashtag', href: 'https://example.com/account/tagged/foo', name: 'Foo' }, - { type: 'Hashtag', href: 'https://example.com/account/tagged/bar', name: 'bar' }, - { type: 'Hashtag', href: 'https://example.com/account/tagged/baz', name: 'baZ' }, - ] - end - - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Collection', - id: collection_url, - items: items, - }.with_indifferent_access - end - - shared_examples 'sets featured tags' do - before do - subject.call(actor, collection_url) - end - - it 'sets expected tags as pinned tags' do - expect(actor.featured_tags.map(&:display_name)).to match_array %w(Foo bar baZ) - end - end - - describe '#call' do - context 'when the endpoint is a Collection' do - before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - end - - it_behaves_like 'sets featured tags' - end - - context 'when the account already has featured tags' do - before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - - actor.featured_tags.create!(name: 'FoO') - actor.featured_tags.create!(name: 'baz') - actor.featured_tags.create!(name: 'oh').update(name: nil) - end - - it_behaves_like 'sets featured tags' - end - - context 'when the endpoint is an OrderedCollection' do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'OrderedCollection', - id: collection_url, - orderedItems: items, - }.with_indifferent_access - end - - before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - end - - it_behaves_like 'sets featured tags' - end - - context 'when the endpoint is a paginated Collection' do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Collection', - id: collection_url, - first: { - type: 'CollectionPage', - partOf: collection_url, - items: items, - }, - }.with_indifferent_access - end - - before do - stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - end - - it_behaves_like 'sets featured tags' - end - end -end diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb deleted file mode 100644 index 175ac9cb61..0000000000 --- a/spec/services/activitypub/fetch_remote_account_service_spec.rb +++ /dev/null @@ -1,137 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::FetchRemoteAccountService do - subject { described_class.new } - - let!(:actor) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://example.com/alice', - type: 'Person', - preferredUsername: 'alice', - name: 'Alice', - summary: 'Foo bar', - inbox: 'http://example.com/alice/inbox', - } - end - - describe '#call' do - let(:account) { subject.call('https://example.com/alice') } - - shared_examples 'sets profile data' do - it 'returns an account with expected details' do - expect(account) - .to be_an(Account) - .and have_attributes( - display_name: eq('Alice'), - note: eq('Foo bar'), - url: eq('https://example.com/alice') - ) - end - end - - context 'when the account does not have a inbox' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } - - before do - actor[:inbox] = nil - - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'fetches resource and looks up webfinger and returns nil' do - expect(account).to be_nil - - expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once - expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once - end - end - - context 'when URI and WebFinger share the same host' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } - - before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'fetches resource and looks up webfinger and sets attributes' do - account - - expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once - expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once - - expect(account.username).to eq 'alice' - expect(account.domain).to eq 'example.com' - end - - include_examples 'sets profile data' - end - - context 'when WebFinger presents different domain than URI' do - let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } - - before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'fetches resource and looks up webfinger and follows redirection and sets attributes' do - account - - expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once - expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once - expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once - - expect(account.username).to eq 'alice' - expect(account.domain).to eq 'iscool.af' - end - - include_examples 'sets profile data' - end - - context 'when WebFinger returns a different URI' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } - - before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'fetches resource and looks up webfinger and does not create account' do - expect(account).to be_nil - - expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once - expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once - end - end - - context 'when WebFinger returns a different URI after a redirection' do - let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } - - before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'fetches resource and looks up webfinger and follows redirect and does not create account' do - expect(account).to be_nil - - expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once - expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once - expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once - end - end - - context 'with wrong id' do - it 'does not create account' do - expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil - end - end - end -end diff --git a/spec/services/activitypub/fetch_remote_actor_service_spec.rb b/spec/services/activitypub/fetch_remote_actor_service_spec.rb deleted file mode 100644 index 9d031cb89b..0000000000 --- a/spec/services/activitypub/fetch_remote_actor_service_spec.rb +++ /dev/null @@ -1,137 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::FetchRemoteActorService do - subject { described_class.new } - - let!(:actor) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://example.com/alice', - type: 'Person', - preferredUsername: 'alice', - name: 'Alice', - summary: 'Foo bar', - inbox: 'http://example.com/alice/inbox', - } - end - - describe '#call' do - let(:account) { subject.call('https://example.com/alice') } - - shared_examples 'sets profile data' do - it 'returns an account and sets attributes' do - expect(account) - .to be_an(Account) - .and have_attributes( - display_name: eq('Alice'), - note: eq('Foo bar'), - url: eq('https://example.com/alice') - ) - end - end - - context 'when the account does not have a inbox' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } - - before do - actor[:inbox] = nil - - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'fetches resource and looks up webfinger and returns nil' do - expect(account).to be_nil - - expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once - expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once - end - end - - context 'when URI and WebFinger share the same host' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } - - before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'fetches resource and looks up webfinger and sets values' do - account - - expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once - expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once - - expect(account.username).to eq 'alice' - expect(account.domain).to eq 'example.com' - end - - include_examples 'sets profile data' - end - - context 'when WebFinger presents different domain than URI' do - let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } - - before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'fetches resource and looks up webfinger and follows redirect and sets values' do - account - - expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once - expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once - expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once - - expect(account.username).to eq 'alice' - expect(account.domain).to eq 'iscool.af' - end - - include_examples 'sets profile data' - end - - context 'when WebFinger returns a different URI' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } - - before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'fetches resource and looks up webfinger and does not create account' do - expect(account).to be_nil - - expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once - expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once - end - end - - context 'when WebFinger returns a different URI after a redirection' do - let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } - - before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'fetches resource and looks up webfinger and follows redirect and does not create account' do - expect(account).to be_nil - - expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once - expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once - expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once - end - end - - context 'with wrong id' do - it 'does not create account' do - expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil - end - end - end -end diff --git a/spec/services/activitypub/fetch_remote_key_service_spec.rb b/spec/services/activitypub/fetch_remote_key_service_spec.rb deleted file mode 100644 index 847a154108..0000000000 --- a/spec/services/activitypub/fetch_remote_key_service_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::FetchRemoteKeyService do - subject { described_class.new } - - let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } - - let(:public_key_pem) do - <<~TEXT - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3L4vnpNLzVH31MeWI39 - 4F0wKeJFsLDAsNXGeOu0QF2x+h1zLWZw/agqD2R3JPU9/kaDJGPIV2Sn5zLyUA9S - 6swCCMOtn7BBR9g9sucgXJmUFB0tACH2QSgHywMAybGfmSb3LsEMNKsGJ9VsvYoh - 8lDET6X4Pyw+ZJU0/OLo/41q9w+OrGtlsTm/PuPIeXnxa6BLqnDaxC+4IcjG/FiP - ahNCTINl/1F/TgSSDZ4Taf4U9XFEIFw8wmgploELozzIzKq+t8nhQYkgAkt64euW - pva3qL5KD1mTIZQEP+LZvh3s2WHrLi3fhbdRuwQ2c0KkJA2oSTFPDpqqbPGZ3Qvu - HQIDAQAB - -----END PUBLIC KEY----- - TEXT - end - - let(:public_key_id) { 'https://example.com/alice#main-key' } - - let(:key_json) do - { - id: public_key_id, - owner: 'https://example.com/alice', - publicKeyPem: public_key_pem, - } - end - - let(:actor_public_key) { key_json } - - let(:actor) do - { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - ], - id: 'https://example.com/alice', - type: 'Person', - preferredUsername: 'alice', - name: 'Alice', - summary: 'Foo bar', - inbox: 'http://example.com/alice/inbox', - publicKey: actor_public_key, - } - end - - before do - stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - describe '#call' do - let(:account) { subject.call(public_key_id) } - - context 'when the key is a sub-object from the actor' do - before do - stub_request(:get, public_key_id).to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) - end - - it 'returns the expected account' do - expect(account.uri).to eq 'https://example.com/alice' - end - end - - context 'when the key is a separate document' do - let(:public_key_id) { 'https://example.com/alice-public-key.json' } - - before do - stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' }) - end - - it 'returns the expected account' do - expect(account.uri).to eq 'https://example.com/alice' - end - end - - context 'when the key and owner do not match' do - let(:public_key_id) { 'https://example.com/fake-public-key.json' } - let(:actor_public_key) { 'https://example.com/alice-public-key.json' } - - before do - stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' }) - end - - it 'returns the nil' do - expect(account).to be_nil - end - end - end -end diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb deleted file mode 100644 index 635fcb7976..0000000000 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ /dev/null @@ -1,317 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::FetchRemoteStatusService do - include ActionView::Helpers::TextHelper - - subject { described_class.new } - - let!(:sender) { Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar') } - - let(:existing_status) { nil } - - let(:note) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://foo.bar/@foo/1234', - type: 'Note', - content: 'Lorem ipsum', - attributedTo: ActivityPub::TagManager.instance.uri_for(sender), - } - end - - before do - stub_request(:get, 'https://foo.bar/watch?v=12345').to_return(status: 404, body: '') - stub_request(:get, object[:id]).to_return(body: Oj.dump(object)) - end - - describe '#call' do - before do - existing_status - subject.call(object[:id], prefetched_body: Oj.dump(object)) - end - - context 'with Note object' do - let(:object) { note } - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.text).to eq 'Lorem ipsum' - end - end - - context 'with Video object' do - let(:object) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://foo.bar/@foo/1234', - type: 'Video', - name: 'Nyan Cat 10 hours remix', - attributedTo: ActivityPub::TagManager.instance.uri_for(sender), - url: [ - { - type: 'Link', - mimeType: 'application/x-bittorrent', - href: 'https://foo.bar/12345.torrent', - }, - - { - type: 'Link', - mimeType: 'text/html', - href: 'https://foo.bar/watch?v=12345', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.url).to eq 'https://foo.bar/watch?v=12345' - expect(strip_tags(status.text)).to eq 'Nyan Cat 10 hours remixhttps://foo.bar/watch?v=12345' - end - end - - context 'with Audio object' do - let(:object) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://foo.bar/@foo/1234', - type: 'Audio', - name: 'Nyan Cat 10 hours remix', - attributedTo: ActivityPub::TagManager.instance.uri_for(sender), - url: [ - { - type: 'Link', - mimeType: 'application/x-bittorrent', - href: 'https://foo.bar/12345.torrent', - }, - - { - type: 'Link', - mimeType: 'text/html', - href: 'https://foo.bar/watch?v=12345', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.url).to eq 'https://foo.bar/watch?v=12345' - expect(strip_tags(status.text)).to eq 'Nyan Cat 10 hours remixhttps://foo.bar/watch?v=12345' - end - end - - context 'with Event object' do - let(:object) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://foo.bar/@foo/1234', - type: 'Event', - name: "Let's change the world", - attributedTo: ActivityPub::TagManager.instance.uri_for(sender), - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.url).to eq 'https://foo.bar/@foo/1234' - expect(strip_tags(status.text)).to eq "Let's change the worldhttps://foo.bar/@foo/1234" - end - end - - context 'with wrong id' do - let(:note) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://real.address/@foo/1234', - type: 'Note', - content: 'Lorem ipsum', - attributedTo: ActivityPub::TagManager.instance.uri_for(sender), - } - end - - let(:object) do - temp = note.dup - temp[:id] = 'https://fake.address/@foo/5678' - temp - end - - it 'does not create status' do - expect(sender.statuses.first).to be_nil - end - end - - context 'with a valid Create activity' do - let(:object) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://foo.bar/@foo/1234/create', - type: 'Create', - actor: ActivityPub::TagManager.instance.uri_for(sender), - object: note, - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.uri).to eq note[:id] - expect(status.text).to eq note[:content] - end - end - - context 'with a Create activity with a mismatching id' do - let(:object) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://foo.bar/@foo/1234/create', - type: 'Create', - actor: ActivityPub::TagManager.instance.uri_for(sender), - object: { - id: 'https://real.address/@foo/1234', - type: 'Note', - content: 'Lorem ipsum', - attributedTo: ActivityPub::TagManager.instance.uri_for(sender), - }, - } - end - - it 'does not create status' do - expect(sender.statuses.first).to be_nil - end - end - - context 'when status already exists' do - let(:existing_status) { Fabricate(:status, account: sender, text: 'Foo', uri: note[:id]) } - - context 'with a Note object' do - let(:object) { note.merge(updated: '2021-09-08T22:39:25Z') } - - it 'updates status' do - existing_status.reload - expect(existing_status.text).to eq 'Lorem ipsum' - expect(existing_status.edits).to_not be_empty - end - end - - context 'with a Create activity' do - let(:object) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://foo.bar/@foo/1234/create', - type: 'Create', - actor: ActivityPub::TagManager.instance.uri_for(sender), - object: note.merge(updated: '2021-09-08T22:39:25Z'), - } - end - - it 'updates status' do - existing_status.reload - expect(existing_status.text).to eq 'Lorem ipsum' - expect(existing_status.edits).to_not be_empty - end - end - end - end - - context 'with statuses referencing other statuses', :inline_jobs do - before do - stub_const 'ActivityPub::FetchRemoteStatusService::DISCOVERIES_PER_REQUEST', 3 - end - - context 'when using inReplyTo' do - let(:object) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://foo.bar/@foo/1', - type: 'Note', - content: 'Lorem ipsum', - inReplyTo: 'https://foo.bar/@foo/2', - attributedTo: ActivityPub::TagManager.instance.uri_for(sender), - } - end - - before do - 5.times do |i| - status_json = { - '@context': 'https://www.w3.org/ns/activitystreams', - id: "https://foo.bar/@foo/#{i}", - type: 'Note', - content: 'Lorem ipsum', - inReplyTo: "https://foo.bar/@foo/#{i + 1}", - attributedTo: ActivityPub::TagManager.instance.uri_for(sender), - to: 'as:Public', - }.with_indifferent_access - stub_request(:get, "https://foo.bar/@foo/#{i}").to_return(status: 200, body: status_json.to_json, headers: { 'Content-Type': 'application/activity+json' }) - end - end - - it 'creates statuses but not more than limit allows' do - expect { subject.call(object[:id], prefetched_body: Oj.dump(object)) } - .to change { sender.statuses.count }.by_at_least(2) - .and change { sender.statuses.count }.by_at_most(3) - end - end - - context 'when using replies' do - let(:object) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://foo.bar/@foo/1', - type: 'Note', - content: 'Lorem ipsum', - replies: { - type: 'Collection', - id: 'https://foo.bar/@foo/1/replies', - first: { - type: 'CollectionPage', - partOf: 'https://foo.bar/@foo/1/replies', - items: ['https://foo.bar/@foo/2'], - }, - }, - attributedTo: ActivityPub::TagManager.instance.uri_for(sender), - } - end - - before do - 5.times do |i| - status_json = { - '@context': 'https://www.w3.org/ns/activitystreams', - id: "https://foo.bar/@foo/#{i}", - type: 'Note', - content: 'Lorem ipsum', - replies: { - type: 'Collection', - id: "https://foo.bar/@foo/#{i}/replies", - first: { - type: 'CollectionPage', - partOf: "https://foo.bar/@foo/#{i}/replies", - items: ["https://foo.bar/@foo/#{i + 1}"], - }, - }, - attributedTo: ActivityPub::TagManager.instance.uri_for(sender), - to: 'as:Public', - }.with_indifferent_access - stub_request(:get, "https://foo.bar/@foo/#{i}").to_return(status: 200, body: status_json.to_json, headers: { 'Content-Type': 'application/activity+json' }) - end - end - - it 'creates statuses but not more than limit allows' do - expect { subject.call(object[:id], prefetched_body: Oj.dump(object)) } - .to change { sender.statuses.count }.by_at_least(2) - .and change { sender.statuses.count }.by_at_most(3) - end - end - end -end diff --git a/spec/services/activitypub/fetch_replies_service_spec.rb b/spec/services/activitypub/fetch_replies_service_spec.rb deleted file mode 100644 index e7d8d3528a..0000000000 --- a/spec/services/activitypub/fetch_replies_service_spec.rb +++ /dev/null @@ -1,148 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::FetchRepliesService do - subject { described_class.new } - - let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') } - let(:status) { Fabricate(:status, account: actor) } - let(:collection_uri) { 'http://example.com/replies/1' } - - let(:items) do - [ - 'http://example.com/self-reply-1', - 'http://example.com/self-reply-2', - 'http://example.com/self-reply-3', - 'http://other.com/other-reply-1', - 'http://other.com/other-reply-2', - 'http://other.com/other-reply-3', - 'http://example.com/self-reply-4', - 'http://example.com/self-reply-5', - 'http://example.com/self-reply-6', - ] - end - - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Collection', - id: collection_uri, - items: items, - }.with_indifferent_access - end - - describe '#call' do - context 'when the payload is a Collection with inlined replies' do - context 'when there is a single reply, with the array compacted away' do - let(:items) { 'http://example.com/self-reply-1' } - - it 'queues the expected worker' do - allow(FetchReplyWorker).to receive(:push_bulk) - - subject.call(status, payload) - - expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1']) - end - end - - context 'when passing the collection itself' do - it 'spawns workers for up to 5 replies on the same server' do - allow(FetchReplyWorker).to receive(:push_bulk) - - subject.call(status, payload) - - expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) - end - end - - context 'when passing the URL to the collection' do - before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - end - - it 'spawns workers for up to 5 replies on the same server' do - allow(FetchReplyWorker).to receive(:push_bulk) - - subject.call(status, collection_uri) - - expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) - end - end - end - - context 'when the payload is an OrderedCollection with inlined replies' do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'OrderedCollection', - id: collection_uri, - orderedItems: items, - }.with_indifferent_access - end - - context 'when passing the collection itself' do - it 'spawns workers for up to 5 replies on the same server' do - allow(FetchReplyWorker).to receive(:push_bulk) - - subject.call(status, payload) - - expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) - end - end - - context 'when passing the URL to the collection' do - before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - end - - it 'spawns workers for up to 5 replies on the same server' do - allow(FetchReplyWorker).to receive(:push_bulk) - - subject.call(status, collection_uri) - - expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) - end - end - end - - context 'when the payload is a paginated Collection with inlined replies' do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Collection', - id: collection_uri, - first: { - type: 'CollectionPage', - partOf: collection_uri, - items: items, - }, - }.with_indifferent_access - end - - context 'when passing the collection itself' do - it 'spawns workers for up to 5 replies on the same server' do - allow(FetchReplyWorker).to receive(:push_bulk) - - subject.call(status, payload) - - expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) - end - end - - context 'when passing the URL to the collection' do - before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - end - - it 'spawns workers for up to 5 replies on the same server' do - allow(FetchReplyWorker).to receive(:push_bulk) - - subject.call(status, collection_uri) - - expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) - end - end - end - end -end diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb deleted file mode 100644 index 86314e6b48..0000000000 --- a/spec/services/activitypub/process_account_service_spec.rb +++ /dev/null @@ -1,243 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::ProcessAccountService do - subject { described_class.new } - - context 'with property values, an avatar, and a profile header' do - let(:payload) do - { - id: 'https://foo.test', - type: 'Actor', - inbox: 'https://foo.test/inbox', - attachment: [ - { type: 'PropertyValue', name: 'Pronouns', value: 'They/them' }, - { type: 'PropertyValue', name: 'Occupation', value: 'Unit test' }, - { type: 'PropertyValue', name: 'non-string', value: %w(foo bar) }, - ], - image: { - type: 'Image', - mediaType: 'image/png', - url: 'https://foo.test/image.png', - }, - icon: { - type: 'Image', - url: [ - { - mediaType: 'image/png', - href: 'https://foo.test/icon.png', - }, - ], - }, - }.with_indifferent_access - end - - before do - stub_request(:get, 'https://foo.test/image.png').to_return(request_fixture('avatar.txt')) - stub_request(:get, 'https://foo.test/icon.png').to_return(request_fixture('avatar.txt')) - end - - it 'parses property values, avatar and profile header as expected' do - account = subject.call('alice', 'example.com', payload) - - expect(account.fields) - .to be_an(Array) - .and have_attributes(size: 2) - expect(account.fields.first) - .to be_an(Account::Field) - .and have_attributes( - name: eq('Pronouns'), - value: eq('They/them') - ) - expect(account.fields.last) - .to be_an(Account::Field) - .and have_attributes( - name: eq('Occupation'), - value: eq('Unit test') - ) - expect(account).to have_attributes( - avatar_remote_url: 'https://foo.test/icon.png', - header_remote_url: 'https://foo.test/image.png' - ) - end - end - - context 'when account is not suspended' do - subject { described_class.new.call(account.username, account.domain, payload) } - - let!(:account) { Fabricate(:account, username: 'alice', domain: 'example.com') } - - let(:payload) do - { - id: 'https://foo.test', - type: 'Actor', - inbox: 'https://foo.test/inbox', - suspended: true, - }.with_indifferent_access - end - - before do - allow(Admin::SuspensionWorker).to receive(:perform_async) - end - - it 'suspends account remotely' do - expect(subject.suspended?).to be true - expect(subject.suspension_origin_remote?).to be true - end - - it 'queues suspension worker' do - subject - expect(Admin::SuspensionWorker).to have_received(:perform_async) - end - end - - context 'when account is suspended' do - subject { described_class.new.call('alice', 'example.com', payload) } - - let!(:account) { Fabricate(:account, username: 'alice', domain: 'example.com', display_name: '') } - - let(:payload) do - { - id: 'https://foo.test', - type: 'Actor', - inbox: 'https://foo.test/inbox', - suspended: false, - name: 'Hoge', - }.with_indifferent_access - end - - before do - allow(Admin::UnsuspensionWorker).to receive(:perform_async) - - account.suspend!(origin: suspension_origin) - end - - context 'when locally' do - let(:suspension_origin) { :local } - - it 'does not unsuspend it' do - expect(subject.suspended?).to be true - end - - it 'does not update any attributes' do - expect(subject.display_name).to_not eq 'Hoge' - end - end - - context 'when remotely' do - let(:suspension_origin) { :remote } - - it 'unsuspends it' do - expect(subject.suspended?).to be false - end - - it 'queues unsuspension worker' do - subject - expect(Admin::UnsuspensionWorker).to have_received(:perform_async) - end - - it 'updates attributes' do - expect(subject.display_name).to eq 'Hoge' - end - end - end - - context 'when discovering many subdomains in a short timeframe' do - subject do - 8.times do |i| - domain = "test#{i}.testdomain.com" - json = { - id: "https://#{domain}/users/1", - type: 'Actor', - inbox: "https://#{domain}/inbox", - }.with_indifferent_access - described_class.new.call('alice', domain, json) - end - end - - before do - stub_const 'ActivityPub::ProcessAccountService::SUBDOMAINS_RATELIMIT', 5 - end - - it 'creates accounts without exceeding rate limit' do - expect { subject } - .to create_some_remote_accounts - .and create_fewer_than_rate_limit_accounts - end - end - - context 'when Accounts referencing other accounts' do - let(:payload) do - { - '@context': ['https://www.w3.org/ns/activitystreams'], - id: 'https://foo.test/users/1', - type: 'Person', - inbox: 'https://foo.test/inbox', - featured: 'https://foo.test/users/1/featured', - preferredUsername: 'user1', - }.with_indifferent_access - end - - before do - stub_const 'ActivityPub::ProcessAccountService::DISCOVERIES_PER_REQUEST', 5 - - 8.times do |i| - actor_json = { - '@context': ['https://www.w3.org/ns/activitystreams'], - id: "https://foo.test/users/#{i}", - type: 'Person', - inbox: 'https://foo.test/inbox', - featured: "https://foo.test/users/#{i}/featured", - preferredUsername: "user#{i}", - }.with_indifferent_access - status_json = { - '@context': ['https://www.w3.org/ns/activitystreams'], - id: "https://foo.test/users/#{i}/status", - attributedTo: "https://foo.test/users/#{i}", - type: 'Note', - content: "@user#{i + 1} test", - tag: [ - { - type: 'Mention', - href: "https://foo.test/users/#{i + 1}", - name: "@user#{i + 1}", - }, - ], - to: ['as:Public', "https://foo.test/users/#{i + 1}"], - }.with_indifferent_access - featured_json = { - '@context': ['https://www.w3.org/ns/activitystreams'], - id: "https://foo.test/users/#{i}/featured", - type: 'OrderedCollection', - totalItems: 1, - orderedItems: [status_json], - }.with_indifferent_access - webfinger = { - subject: "acct:user#{i}@foo.test", - links: [{ rel: 'self', href: "https://foo.test/users/#{i}", type: 'application/activity+json' }], - }.with_indifferent_access - stub_request(:get, "https://foo.test/users/#{i}").to_return(status: 200, body: actor_json.to_json, headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, "https://foo.test/users/#{i}/featured").to_return(status: 200, body: featured_json.to_json, headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, "https://foo.test/users/#{i}/status").to_return(status: 200, body: status_json.to_json, headers: { 'Content-Type': 'application/activity+json' }) - stub_request(:get, "https://foo.test/.well-known/webfinger?resource=acct:user#{i}@foo.test").to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' }) - end - end - - it 'creates accounts without exceeding rate limit', :inline_jobs do - expect { subject.call('user1', 'foo.test', payload) } - .to create_some_remote_accounts - .and create_fewer_than_rate_limit_accounts - end - end - - private - - def create_some_remote_accounts - change(Account.remote, :count).by_at_least(2) - end - - def create_fewer_than_rate_limit_accounts - change(Account.remote, :count).by_at_most(5) - end -end diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb deleted file mode 100644 index 74df0f9106..0000000000 --- a/spec/services/activitypub/process_collection_service_spec.rb +++ /dev/null @@ -1,273 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::ProcessCollectionService do - subject { described_class.new } - - let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') } - - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Create', - actor: ActivityPub::TagManager.instance.uri_for(actor), - object: { - id: 'bar', - type: 'Note', - content: 'Lorem ipsum', - }, - } - end - - let(:json) { Oj.dump(payload) } - - describe '#call' do - context 'when actor is suspended' do - before do - actor.suspend!(origin: :remote) - end - - %w(Accept Add Announce Block Create Flag Follow Like Move Remove).each do |activity_type| - context "with #{activity_type} activity" do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: activity_type, - actor: ActivityPub::TagManager.instance.uri_for(actor), - } - end - - it 'does not process payload' do - allow(ActivityPub::Activity).to receive(:factory) - - subject.call(json, actor) - - expect(ActivityPub::Activity).to_not have_received(:factory) - end - end - end - - %w(Delete Reject Undo Update).each do |activity_type| - context "with #{activity_type} activity" do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: activity_type, - actor: ActivityPub::TagManager.instance.uri_for(actor), - } - end - - it 'processes the payload' do - allow(ActivityPub::Activity).to receive(:factory) - - subject.call(json, actor) - - expect(ActivityPub::Activity).to have_received(:factory) - end - end - end - end - - context 'when actor differs from sender' do - let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') } - - it 'does not process payload if no signature exists' do - signature_double = instance_double(ActivityPub::LinkedDataSignature, verify_actor!: nil) - allow(ActivityPub::LinkedDataSignature).to receive(:new).and_return(signature_double) - allow(ActivityPub::Activity).to receive(:factory) - - subject.call(json, forwarder) - - expect(ActivityPub::Activity).to_not have_received(:factory) - end - - it 'processes payload with actor if valid signature exists' do - payload['signature'] = { 'type' => 'RsaSignature2017' } - - signature_double = instance_double(ActivityPub::LinkedDataSignature, verify_actor!: actor) - allow(ActivityPub::LinkedDataSignature).to receive(:new).and_return(signature_double) - allow(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor, instance_of(Hash)) - - subject.call(json, forwarder) - - expect(ActivityPub::Activity).to have_received(:factory).with(instance_of(Hash), actor, instance_of(Hash)) - end - - it 'does not process payload if invalid signature exists' do - payload['signature'] = { 'type' => 'RsaSignature2017' } - - signature_double = instance_double(ActivityPub::LinkedDataSignature, verify_actor!: nil) - allow(ActivityPub::LinkedDataSignature).to receive(:new).and_return(signature_double) - allow(ActivityPub::Activity).to receive(:factory) - - subject.call(json, forwarder) - - expect(ActivityPub::Activity).to_not have_received(:factory) - end - - context 'when receiving a fabricated status' do - let!(:actor) do - Fabricate(:account, - username: 'bob', - domain: 'example.com', - uri: 'https://example.com/users/bob', - private_key: nil, - public_key: <<~TEXT) - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuuYyoyfsRkYnXRotMsId - W3euBDDfiv9oVqOxUVC7bhel8KednIMrMCRWFAkgJhbrlzbIkjVr68o1MP9qLcn7 - CmH/BXHp7yhuFTr4byjdJKpwB+/i2jNEsvDH5jR8WTAeTCe0x/QHg21V3F7dSI5m - CCZ/1dSIyOXLRTWVlfDlm3rE4ntlCo+US3/7oSWbg/4/4qEnt1HC32kvklgScxua - 4LR5ATdoXa5bFoopPWhul7MJ6NyWCyQyScUuGdlj8EN4kmKQJvphKHrI9fvhgOuG - TvhTR1S5InA4azSSchY0tXEEw/VNxraeX0KPjbgr6DPcwhPd/m0nhVDq0zVyVBBD - MwIDAQAB - -----END PUBLIC KEY----- - TEXT - end - - let(:payload) do - { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - nil, - { object: 'https://www.w3.org/ns/activitystreams#object' }, - ], - id: 'https://example.com/users/bob/fake-status/activity', - type: 'Create', - actor: 'https://example.com/users/bob', - published: '2022-01-22T15:00:00Z', - to: [ - 'https://www.w3.org/ns/activitystreams#Public', - ], - cc: [ - 'https://example.com/users/bob/followers', - ], - signature: { - type: 'RsaSignature2017', - creator: 'https://example.com/users/bob#main-key', - created: '2022-03-09T21:57:25Z', - signatureValue: 'WculK0LelTQ0MvGwU9TPoq5pFzFfGYRDCJqjZ232/Udj4' \ - 'CHqDTGOSw5UTDLShqBOyycCkbZGrQwXG+dpyDpQLSe1UV' \ - 'PZ5TPQtc/9XtI57WlS2nMNpdvRuxGnnb2btPdesXZ7n3p' \ - 'Cxo0zjaXrJMe0mqQh5QJO22mahb4bDwwmfTHgbD3nmkD+' \ - 'fBfGi+UV2qWwqr+jlV4L4JqNkh0gWljF5KTePLRRZCuWi' \ - 'Q/FAt7c67636cdIPf7fR+usjuZltTQyLZKEGuK8VUn2Gk' \ - 'fsx5qns7Vcjvlz1JqlAjyO8HPBbzTTHzUG2nUOIgC3Poj' \ - 'CSWv6mNTmRGoLZzOscCAYQA6cKw==', - }, - '@id': 'https://example.com/users/bob/statuses/107928807471117876/activity', - '@type': 'https://www.w3.org/ns/activitystreams#Create', - 'https://www.w3.org/ns/activitystreams#actor': { - '@id': 'https://example.com/users/bob', - }, - 'https://www.w3.org/ns/activitystreams#cc': { - '@id': 'https://example.com/users/bob/followers', - }, - object: { - id: 'https://example.com/users/bob/fake-status', - type: 'Note', - published: '2022-01-22T15:00:00Z', - url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=puck-was-here', - attributedTo: 'https://example.com/users/bob', - to: [ - 'https://www.w3.org/ns/activitystreams#Public', - ], - cc: [ - 'https://example.com/users/bob/followers', - ], - sensitive: false, - atomUri: 'https://example.com/users/bob/fake-status', - conversation: 'tag:example.com,2022-03-09:objectId=15:objectType=Conversation', - content: '

puck was here

', - - '@id': 'https://example.com/users/bob/statuses/107928807471117876', - '@type': 'https://www.w3.org/ns/activitystreams#Note', - 'http://ostatus.org#atomUri': 'https://example.com/users/bob/statuses/107928807471117876', - 'http://ostatus.org#conversation': 'tag:example.com,2022-03-09:objectId=15:objectType=Conversation', - 'https://www.w3.org/ns/activitystreams#attachment': [], - 'https://www.w3.org/ns/activitystreams#attributedTo': { - '@id': 'https://example.com/users/bob', - }, - 'https://www.w3.org/ns/activitystreams#cc': { - '@id': 'https://example.com/users/bob/followers', - }, - 'https://www.w3.org/ns/activitystreams#content': [ - '

hello world

', - { - '@value': '

hello world

', - '@language': 'en', - }, - ], - 'https://www.w3.org/ns/activitystreams#published': { - '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', - '@value': '2022-03-09T21:55:07Z', - }, - 'https://www.w3.org/ns/activitystreams#replies': { - '@id': 'https://example.com/users/bob/statuses/107928807471117876/replies', - '@type': 'https://www.w3.org/ns/activitystreams#Collection', - 'https://www.w3.org/ns/activitystreams#first': { - '@type': 'https://www.w3.org/ns/activitystreams#CollectionPage', - 'https://www.w3.org/ns/activitystreams#items': [], - 'https://www.w3.org/ns/activitystreams#next': { - '@id': 'https://example.com/users/bob/statuses/107928807471117876/replies?only_other_accounts=true&page=true', - }, - 'https://www.w3.org/ns/activitystreams#partOf': { - '@id': 'https://example.com/users/bob/statuses/107928807471117876/replies', - }, - }, - }, - 'https://www.w3.org/ns/activitystreams#sensitive': false, - 'https://www.w3.org/ns/activitystreams#tag': [], - 'https://www.w3.org/ns/activitystreams#to': { - '@id': 'https://www.w3.org/ns/activitystreams#Public', - }, - 'https://www.w3.org/ns/activitystreams#url': { - '@id': 'https://example.com/@bob/107928807471117876', - }, - }, - 'https://www.w3.org/ns/activitystreams#published': { - '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', - '@value': '2022-03-09T21:55:07Z', - }, - 'https://www.w3.org/ns/activitystreams#to': { - '@id': 'https://www.w3.org/ns/activitystreams#Public', - }, - } - end - - it 'does not process forged payload' do - allow(ActivityPub::Activity).to receive(:factory) - - expect { subject.call(json, forwarder) } - .to_not change(actor.reload.statuses, :count) - - expect(ActivityPub::Activity).to_not have_received(:factory).with( - hash_including( - 'object' => hash_including( - 'id' => 'https://example.com/users/bob/fake-status' - ) - ), - anything, - anything - ) - - expect(ActivityPub::Activity).to_not have_received(:factory).with( - hash_including( - 'object' => hash_including( - 'content' => '

puck was here

' - ) - ), - anything, - anything - ) - - expect(Status.exists?(uri: 'https://example.com/users/bob/fake-status')).to be false - end - end - end - end -end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb deleted file mode 100644 index a97e840802..0000000000 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ /dev/null @@ -1,422 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -def poll_option_json(name, votes) - { type: 'Note', name: name, replies: { type: 'Collection', totalItems: votes } } -end - -RSpec.describe ActivityPub::ProcessStatusUpdateService do - subject { described_class.new } - - let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) } - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Note', - summary: 'Show more', - content: 'Hello universe', - updated: '2021-09-08T22:39:25Z', - tag: [ - { type: 'Hashtag', name: 'hoge' }, - { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, - ], - } - end - let(:json) { Oj.load(Oj.dump(payload)) } - - let(:alice) { Fabricate(:account) } - let(:bob) { Fabricate(:account) } - - let(:mentions) { [] } - let(:tags) { [] } - let(:media_attachments) { [] } - - before do - mentions.each { |a| Fabricate(:mention, status: status, account: a) } - tags.each { |t| status.tags << t } - media_attachments.each { |m| status.media_attachments << m } - end - - describe '#call' do - it 'updates text and content warning' do - subject.call(status, json, json) - expect(status.reload) - .to have_attributes( - text: eq('Hello universe'), - spoiler_text: eq('Show more') - ) - end - - context 'when the changes are only in sanitized-out HTML' do - let!(:status) { Fabricate(:status, text: '

Hello world joinmastodon.org

', account: Fabricate(:account, domain: 'example.com')) } - - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Note', - updated: '2021-09-08T22:39:25Z', - content: '

Hello world joinmastodon.org

', - } - end - - before do - subject.call(status, json, json) - end - - it 'does not create any edits and does not mark status edited' do - expect(status.reload.edits).to be_empty - expect(status).to_not be_edited - end - end - - context 'when the status has not been explicitly edited' do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Note', - content: 'Updated text', - } - end - - before do - subject.call(status, json, json) - end - - it 'does not create any edits, mark status edited, or update text' do - expect(status.reload.edits).to be_empty - expect(status.reload).to_not be_edited - expect(status.reload.text).to eq 'Hello world' - end - end - - context 'when the status has not been explicitly edited and features a poll' do - let(:account) { Fabricate(:account, domain: 'example.com') } - let!(:expiration) { 10.days.from_now.utc } - let!(:status) do - Fabricate(:status, - text: 'Hello world', - account: account, - poll_attributes: { - options: %w(Foo Bar), - account: account, - multiple: false, - hide_totals: false, - expires_at: expiration, - }) - end - - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://example.com/foo', - type: 'Question', - content: 'Hello world', - endTime: expiration.iso8601, - oneOf: [ - poll_option_json('Foo', 4), - poll_option_json('Bar', 3), - ], - } - end - - before do - subject.call(status, json, json) - end - - it 'does not create any edits, mark status edited, update text but does update tallies' do - expect(status.reload.edits).to be_empty - expect(status.reload).to_not be_edited - expect(status.reload.text).to eq 'Hello world' - expect(status.poll.reload.cached_tallies).to eq [4, 3] - end - end - - context 'when the status changes a poll despite being not explicitly marked as updated' do - let(:account) { Fabricate(:account, domain: 'example.com') } - let!(:expiration) { 10.days.from_now.utc } - let!(:status) do - Fabricate(:status, - text: 'Hello world', - account: account, - poll_attributes: { - options: %w(Foo Bar), - account: account, - multiple: false, - hide_totals: false, - expires_at: expiration, - }) - end - - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://example.com/foo', - type: 'Question', - content: 'Hello world', - endTime: expiration.iso8601, - oneOf: [ - poll_option_json('Foo', 4), - poll_option_json('Bar', 3), - poll_option_json('Baz', 3), - ], - } - end - - before do - subject.call(status, json, json) - end - - it 'does not create any edits, mark status edited, update text, or update tallies' do - expect(status.reload.edits).to be_empty - expect(status.reload).to_not be_edited - expect(status.reload.text).to eq 'Hello world' - expect(status.poll.reload.cached_tallies).to eq [0, 0] - end - end - - context 'when receiving an edit older than the latest processed' do - before do - status.snapshot!(at_time: status.created_at, rate_limit: false) - status.update!(text: 'Hello newer world', edited_at: Time.now.utc) - status.snapshot!(rate_limit: false) - end - - it 'does not create any edits or update relevant attributes' do - expect { subject.call(status, json, json) } - .to not_change { status.reload.edits.pluck(&:id) } - .and(not_change { status.reload.attributes.slice('text', 'spoiler_text', 'edited_at').values }) - end - end - - context 'with no changes at all' do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Note', - content: 'Hello world', - } - end - - before do - subject.call(status, json, json) - end - - it 'does not create any edits or mark status edited' do - expect(status.reload.edits).to be_empty - expect(status).to_not be_edited - end - end - - context 'with no changes and originally with no ordered_media_attachment_ids' do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Note', - content: 'Hello world', - } - end - - before do - status.update(ordered_media_attachment_ids: nil) - subject.call(status, json, json) - end - - it 'does not create any edits or mark status edited' do - expect(status.reload.edits).to be_empty - expect(status).to_not be_edited - end - end - - context 'when originally without tags' do - before do - subject.call(status, json, json) - end - - it 'updates tags' do - expect(status.tags.reload.map(&:name)).to eq %w(hoge) - end - end - - context 'when originally with tags' do - let(:tags) { [Fabricate(:tag, name: 'test'), Fabricate(:tag, name: 'foo')] } - - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Note', - summary: 'Show more', - content: 'Hello universe', - updated: '2021-09-08T22:39:25Z', - tag: [ - { type: 'Hashtag', name: 'foo' }, - ], - } - end - - before do - subject.call(status, json, json) - end - - it 'updates tags' do - expect(status.tags.reload.map(&:name)).to eq %w(foo) - end - end - - context 'when originally without mentions' do - before do - subject.call(status, json, json) - end - - it 'updates mentions' do - expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id] - end - end - - context 'when originally with mentions' do - let(:mentions) { [alice, bob] } - - before do - subject.call(status, json, json) - end - - it 'updates mentions' do - expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id] - end - end - - context 'when originally without media attachments' do - before do - stub_request(:get, 'https://example.com/foo.png').to_return(body: attachment_fixture('emojo.png')) - subject.call(status, json, json) - end - - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Note', - content: 'Hello universe', - updated: '2021-09-08T22:39:25Z', - attachment: [ - { type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png' }, - ], - } - end - - it 'updates media attachments' do - media_attachment = status.reload.ordered_media_attachments.first - - expect(media_attachment).to_not be_nil - expect(media_attachment.remote_url).to eq 'https://example.com/foo.png' - end - - it 'fetches the attachment' do - expect(a_request(:get, 'https://example.com/foo.png')).to have_been_made - end - - it 'records media change in edit' do - expect(status.edits.reload.last.ordered_media_attachment_ids).to_not be_empty - end - end - - context 'when originally with media attachments' do - let(:media_attachments) { [Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png'), Fabricate(:media_attachment, remote_url: 'https://example.com/unused.png')] } - - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Note', - content: 'Hello universe', - updated: '2021-09-08T22:39:25Z', - attachment: [ - { type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png', name: 'A picture' }, - ], - } - end - - before do - allow(RedownloadMediaWorker).to receive(:perform_async) - subject.call(status, json, json) - end - - it 'updates the existing media attachment in-place' do - media_attachment = status.media_attachments.ordered.reload.first - - expect(media_attachment).to_not be_nil - expect(media_attachment.remote_url).to eq 'https://example.com/foo.png' - expect(media_attachment.description).to eq 'A picture' - end - - it 'does not queue redownload for the existing media attachment' do - expect(RedownloadMediaWorker).to_not have_received(:perform_async) - end - - it 'updates media attachments' do - expect(status.ordered_media_attachments.map(&:remote_url)).to eq %w(https://example.com/foo.png) - end - - it 'records media change in edit' do - expect(status.edits.reload.last.ordered_media_attachment_ids).to_not be_empty - end - end - - context 'when originally with a poll' do - before do - poll = Fabricate(:poll, status: status) - status.update(preloadable_poll: poll) - subject.call(status, json, json) - end - - it 'removes poll and records media change in edit' do - expect(status.reload.poll).to be_nil - expect(status.edits.reload.last.poll_options).to be_nil - end - end - - context 'when originally without a poll' do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Question', - content: 'Hello universe', - updated: '2021-09-08T22:39:25Z', - closed: true, - oneOf: [ - { type: 'Note', name: 'Foo' }, - { type: 'Note', name: 'Bar' }, - { type: 'Note', name: 'Baz' }, - ], - } - end - - before do - subject.call(status, json, json) - end - - it 'creates a poll and records media change in edit' do - poll = status.reload.poll - - expect(poll).to_not be_nil - expect(poll.options).to eq %w(Foo Bar Baz) - expect(status.edits.reload.last.poll_options).to eq %w(Foo Bar Baz) - end - end - - it 'creates edit history and sets edit timestamp' do - subject.call(status, json, json) - expect(status.edits.reload.map(&:text)) - .to eq ['Hello world', 'Hello universe'] - expect(status.reload.edited_at.to_s) - .to eq '2021-09-08 22:39:25 UTC' - end - end -end diff --git a/spec/services/activitypub/synchronize_followers_service_spec.rb b/spec/services/activitypub/synchronize_followers_service_spec.rb deleted file mode 100644 index 974368b7d7..0000000000 --- a/spec/services/activitypub/synchronize_followers_service_spec.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::SynchronizeFollowersService do - subject { described_class.new } - - let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account', inbox_url: 'http://example.com/inbox') } - let(:alice) { Fabricate(:account, username: 'alice') } - let(:bob) { Fabricate(:account, username: 'bob') } - let(:eve) { Fabricate(:account, username: 'eve') } - let(:mallory) { Fabricate(:account, username: 'mallory') } - let(:collection_uri) { 'http://example.com/partial-followers' } - - let(:items) do - [alice, eve, mallory].map do |account| - ActivityPub::TagManager.instance.uri_for(account) - end - end - - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Collection', - id: collection_uri, - items: items, - }.with_indifferent_access - end - - shared_examples 'synchronizes followers' do - before do - alice.follow!(actor) - bob.follow!(actor) - mallory.request_follow!(actor) - - allow(ActivityPub::DeliveryWorker).to receive(:perform_async) - - subject.call(actor, collection_uri) - end - - it 'maintains following records and sends Undo Follow to actor' do - expect(alice) - .to be_following(actor) # Keep expected followers - expect(bob) - .to_not be_following(actor) # Remove local followers not in remote list - expect(mallory) - .to be_following(actor) # Convert follow request to follow when accepted - expect(ActivityPub::DeliveryWorker) - .to have_received(:perform_async).with(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor - end - end - - describe '#call' do - context 'when the endpoint is a Collection of actor URIs' do - before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - end - - it_behaves_like 'synchronizes followers' - end - - context 'when the endpoint is an OrderedCollection of actor URIs' do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'OrderedCollection', - id: collection_uri, - orderedItems: items, - }.with_indifferent_access - end - - before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - end - - it_behaves_like 'synchronizes followers' - end - - context 'when the endpoint is a paginated Collection of actor URIs' do - let(:payload) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Collection', - id: collection_uri, - first: { - type: 'CollectionPage', - partOf: collection_uri, - items: items, - }, - }.with_indifferent_access - end - - before do - stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) - end - - it_behaves_like 'synchronizes followers' - end - end -end diff --git a/spec/services/after_block_domain_from_account_service_spec.rb b/spec/services/after_block_domain_from_account_service_spec.rb deleted file mode 100644 index 248648a809..0000000000 --- a/spec/services/after_block_domain_from_account_service_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AfterBlockDomainFromAccountService do - subject { described_class.new } - - let(:wolf) { Fabricate(:account, username: 'wolf', domain: 'evil.org', inbox_url: 'https://evil.org/wolf/inbox', protocol: :activitypub) } - let(:dog) { Fabricate(:account, username: 'dog', domain: 'evil.org', inbox_url: 'https://evil.org/dog/inbox', protocol: :activitypub) } - let(:alice) { Fabricate(:account, username: 'alice') } - - before do - NotificationPermission.create!(account: alice, from_account: wolf) - - wolf.follow!(alice) - alice.follow!(dog) - end - - it 'purge followers from blocked domain, remove notification permissions, sends `Reject->Follow`, and records severed relationships', :aggregate_failures do - expect { subject.call(alice, 'evil.org') } - .to change { wolf.following?(alice) }.from(true).to(false) - .and change { NotificationPermission.exists?(account: alice, from_account: wolf) }.from(true).to(false) - - expect(ActivityPub::DeliveryWorker.jobs.pluck('args')).to contain_exactly( - [a_string_including('"type":"Reject"'), alice.id, wolf.inbox_url], - [a_string_including('"type":"Undo"'), alice.id, dog.inbox_url] - ) - - severed_relationships = alice.severed_relationships.to_a - expect(severed_relationships.count).to eq 2 - expect(severed_relationships[0].relationship_severance_event).to eq severed_relationships[1].relationship_severance_event - expect(severed_relationships.map { |rel| [rel.account, rel.target_account] }).to contain_exactly([wolf, alice], [alice, dog]) - end -end diff --git a/spec/services/after_block_service_spec.rb b/spec/services/after_block_service_spec.rb deleted file mode 100644 index 82825dad98..0000000000 --- a/spec/services/after_block_service_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AfterBlockService do - subject { described_class.new.call(account, target_account) } - - let(:account) { Fabricate(:account) } - let(:target_account) { Fabricate(:account) } - let(:status) { Fabricate(:status, account: target_account) } - let(:other_status) { Fabricate(:status, account: target_account) } - let(:other_account_status) { Fabricate(:status) } - let(:other_account_reblog) { Fabricate(:status, reblog_of_id: other_status.id) } - - describe 'home timeline' do - let(:home_timeline_key) { FeedManager.instance.key(:home, account.id) } - - before do - redis.del(home_timeline_key) - end - - it "clears account's statuses" do - FeedManager.instance.push_to_home(account, status) - FeedManager.instance.push_to_home(account, other_account_status) - FeedManager.instance.push_to_home(account, other_account_reblog) - - expect { subject }.to change { - redis.zrange(home_timeline_key, 0, -1) - }.from([status.id.to_s, other_account_status.id.to_s, other_account_reblog.id.to_s]).to([other_account_status.id.to_s]) - end - end - - describe 'lists' do - let(:list) { Fabricate(:list, account: account) } - let(:list_timeline_key) { FeedManager.instance.key(:list, list.id) } - - before do - redis.del(list_timeline_key) - end - - it "clears account's statuses" do - FeedManager.instance.push_to_list(list, status) - FeedManager.instance.push_to_list(list, other_account_status) - FeedManager.instance.push_to_list(list, other_account_reblog) - - expect { subject }.to change { - redis.zrange(list_timeline_key, 0, -1) - }.from([status.id.to_s, other_account_status.id.to_s, other_account_reblog.id.to_s]).to([other_account_status.id.to_s]) - end - end -end diff --git a/spec/services/after_unallow_domain_service_spec.rb b/spec/services/after_unallow_domain_service_spec.rb deleted file mode 100644 index 717c42b931..0000000000 --- a/spec/services/after_unallow_domain_service_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AfterUnallowDomainService do - describe '#call' do - context 'with accounts for a domain' do - let!(:account) { Fabricate(:account, domain: 'host.example') } - let!(:test_account) { Fabricate(:account, domain: 'test.example') } - let(:service_double) { instance_double(DeleteAccountService, call: true) } - - before { allow(DeleteAccountService).to receive(:new).and_return(service_double) } - - it 'calls the delete service for accounts from the relevant domain' do - subject.call 'test.example' - - expect(service_double) - .to_not have_received(:call).with(account, reserve_username: false) - expect(service_double) - .to have_received(:call).with(test_account, reserve_username: false) - end - end - end -end diff --git a/spec/services/app_sign_up_service_spec.rb b/spec/services/app_sign_up_service_spec.rb deleted file mode 100644 index ec7b7516f9..0000000000 --- a/spec/services/app_sign_up_service_spec.rb +++ /dev/null @@ -1,130 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AppSignUpService do - subject { described_class.new } - - let(:app) { Fabricate(:application, scopes: 'read write') } - let(:good_params) { { username: 'alice', password: '12345678', email: 'good@email.com', agreement: true } } - let(:remote_ip) { IPAddr.new('198.0.2.1') } - - describe '#call' do - let(:params) { good_params } - - shared_examples 'successful registration' do - it 'creates an unconfirmed user with access token and the app\'s scope', :aggregate_failures do - access_token = subject.call(app, remote_ip, params) - expect(access_token).to_not be_nil - expect(access_token.scopes.to_s).to eq 'read write' - - user = User.find_by(id: access_token.resource_owner_id) - expect(user).to_not be_nil - expect(user.confirmed?).to be false - - expect(user.account).to_not be_nil - expect(user.invite_request).to be_nil - end - end - - context 'when the email address requires approval' do - before do - Setting.registrations_mode = 'open' - Fabricate(:email_domain_block, allow_with_approval: true, domain: 'email.com') - end - - it 'creates an unapproved user', :aggregate_failures do - access_token = subject.call(app, remote_ip, params) - expect(access_token).to_not be_nil - expect(access_token.scopes.to_s).to eq 'read write' - - user = User.find_by(id: access_token.resource_owner_id) - expect(user).to_not be_nil - expect(user.confirmed?).to be false - expect(user.approved?).to be false - - expect(user.account).to_not be_nil - expect(user.invite_request).to be_nil - end - end - - context 'when the email address requires approval through MX records' do - before do - Setting.registrations_mode = 'open' - Fabricate(:email_domain_block, allow_with_approval: true, domain: 'smtp.email.com') - allow(User).to receive(:skip_mx_check?).and_return(false) - - resolver = instance_double(Resolv::DNS, :timeouts= => nil) - - allow(resolver).to receive(:getresources) - .with('email.com', Resolv::DNS::Resource::IN::MX) - .and_return([instance_double(Resolv::DNS::Resource::MX, exchange: 'smtp.email.com')]) - allow(resolver).to receive(:getresources).with('email.com', Resolv::DNS::Resource::IN::A).and_return([]) - allow(resolver).to receive(:getresources).with('email.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) - allow(resolver).to receive(:getresources).with('smtp.email.com', Resolv::DNS::Resource::IN::A).and_return([instance_double(Resolv::DNS::Resource::IN::A, address: '2.3.4.5')]) - allow(resolver).to receive(:getresources).with('smtp.email.com', Resolv::DNS::Resource::IN::AAAA).and_return([instance_double(Resolv::DNS::Resource::IN::AAAA, address: 'fd00::2')]) - allow(Resolv::DNS).to receive(:open).and_yield(resolver) - end - - it 'creates an unapproved user', :aggregate_failures do - access_token = subject.call(app, remote_ip, params) - expect(access_token).to_not be_nil - expect(access_token.scopes.to_s).to eq 'read write' - - user = User.find_by(id: access_token.resource_owner_id) - expect(user).to_not be_nil - expect(user.confirmed?).to be false - expect(user.approved?).to be false - - expect(user.account).to_not be_nil - expect(user.invite_request).to be_nil - end - end - - context 'when registrations are closed' do - before do - Setting.registrations_mode = 'none' - end - - it 'raises an error', :aggregate_failures do - expect { subject.call(app, remote_ip, good_params) }.to raise_error Mastodon::NotPermittedError - end - - context 'when using a valid invite' do - let(:params) { good_params.merge({ invite_code: invite.code }) } - let(:invite) { Fabricate(:invite) } - - before do - invite.user.approve! - end - - it_behaves_like 'successful registration' - end - - context 'when using an invalid invite' do - let(:params) { good_params.merge({ invite_code: invite.code }) } - let(:invite) { Fabricate(:invite, uses: 1, max_uses: 1) } - - it 'raises an error', :aggregate_failures do - expect { subject.call(app, remote_ip, params) }.to raise_error Mastodon::NotPermittedError - end - end - end - - it 'raises an error when params are missing' do - expect { subject.call(app, remote_ip, {}) }.to raise_error ActiveRecord::RecordInvalid - end - - it_behaves_like 'successful registration' - - context 'when given an invite request text' do - it 'creates an account with invite request text' do - access_token = subject.call(app, remote_ip, good_params.merge(reason: 'Foo bar')) - expect(access_token).to_not be_nil - user = User.find_by(id: access_token.resource_owner_id) - expect(user).to_not be_nil - expect(user.invite_request&.text).to eq 'Foo bar' - end - end - end -end diff --git a/spec/services/appeal_service_spec.rb b/spec/services/appeal_service_spec.rb deleted file mode 100644 index 6a47bb2cea..0000000000 --- a/spec/services/appeal_service_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AppealService, :inline_jobs do - describe '#call' do - let!(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - - context 'with an existing strike' do - let(:strike) { Fabricate(:account_warning) } - let(:text) { 'Appeal text' } - - it 'creates an appeal and notifies staff' do - emails = capture_emails { subject.call(strike, text) } - - expect(Appeal.last) - .to have_attributes( - strike: strike, - text: text, - account: strike.target_account - ) - - expect(emails.size) - .to eq(1) - - expect(emails.first) - .to have_attributes( - to: contain_exactly(admin.email), - subject: eq( - I18n.t( - 'admin_mailer.new_appeal.subject', - username: strike.target_account.acct, - instance: Rails.configuration.x.local_domain - ) - ) - ) - end - end - end -end diff --git a/spec/services/approve_appeal_service_spec.rb b/spec/services/approve_appeal_service_spec.rb deleted file mode 100644 index 5707c5d7f4..0000000000 --- a/spec/services/approve_appeal_service_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ApproveAppealService do - describe '#call' do - context 'with an existing appeal' do - let(:appeal) { Fabricate(:appeal) } - let(:account) { Fabricate(:account) } - - it 'processes the appeal approval' do - expect { subject.call(appeal, account) } - .to mark_overruled - .and record_approver - end - - def mark_overruled - change(appeal.strike, :overruled_at) - .from(nil) - .to(be > 1.minute.ago) - end - - def record_approver - change(appeal, :approved_by_account) - .from(nil) - .to(account) - end - end - end -end diff --git a/spec/services/authorize_follow_service_spec.rb b/spec/services/authorize_follow_service_spec.rb deleted file mode 100644 index 533b791fb7..0000000000 --- a/spec/services/authorize_follow_service_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AuthorizeFollowService do - subject { described_class.new } - - let(:sender) { Fabricate(:account, username: 'alice') } - - describe 'local' do - let(:bob) { Fabricate(:account, username: 'bob') } - - before do - FollowRequest.create(account: bob, target_account: sender) - subject.call(bob, sender) - end - - it 'removes follow request' do - expect(bob.requested?(sender)).to be false - end - - it 'creates follow relation' do - expect(bob.following?(sender)).to be true - end - end - - describe 'remote ActivityPub' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } - - before do - FollowRequest.create(account: bob, target_account: sender) - stub_request(:post, bob.inbox_url).to_return(status: 200) - subject.call(bob, sender) - end - - it 'removes follow request' do - expect(bob.requested?(sender)).to be false - end - - it 'creates follow relation' do - expect(bob.following?(sender)).to be true - end - - it 'sends an accept activity', :inline_jobs do - expect(a_request(:post, bob.inbox_url)).to have_been_made.once - end - end -end diff --git a/spec/services/backup_service_spec.rb b/spec/services/backup_service_spec.rb deleted file mode 100644 index 878405a0fe..0000000000 --- a/spec/services/backup_service_spec.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe BackupService do - subject(:service_call) { described_class.new.call(backup) } - - let!(:user) { Fabricate(:user) } - let!(:attachment) { Fabricate(:media_attachment, account: user.account) } - let!(:status) { Fabricate(:status, account: user.account, text: 'Hello', visibility: :public, media_attachments: [attachment]) } - let!(:private_status) { Fabricate(:status, account: user.account, text: 'secret', visibility: :private) } - let!(:favourite) { Fabricate(:favourite, account: user.account) } - let!(:bookmark) { Fabricate(:bookmark, account: user.account) } - let!(:backup) { Fabricate(:backup, user: user) } - - def read_zip_file(backup, filename) - file = Paperclip.io_adapters.for(backup.dump) - Zip::File.open(file) do |zipfile| - entry = zipfile.glob(filename).first - return entry.get_input_stream.read - end - end - - context 'when the user has an avatar and header' do - before do - user.account.update!(avatar: attachment_fixture('avatar.gif')) - user.account.update!(header: attachment_fixture('emojo.png')) - end - - it 'stores them as expected' do - service_call - - json = export_json(:actor) - avatar_path = json.dig('icon', 'url') - header_path = json.dig('image', 'url') - - expect(avatar_path).to_not be_nil - expect(header_path).to_not be_nil - - expect(read_zip_file(backup, avatar_path)).to be_present - expect(read_zip_file(backup, header_path)).to be_present - end - end - - it 'marks the backup as processed and exports files' do - expect { service_call }.to process_backup - - expect_outbox_export - expect_likes_export - expect_bookmarks_export - end - - def process_backup - change(backup, :processed).from(false).to(true) - end - - def expect_outbox_export - body = export_json_raw(:outbox) - json = Oj.load(body) - - aggregate_failures do - expect(body.scan('@context').count).to eq 1 - expect(body.scan('orderedItems').count).to eq 1 - expect(json['@context']).to_not be_nil - expect(json['type']).to eq 'OrderedCollection' - expect(json['totalItems']).to eq 2 - expect(json['orderedItems'][0]['@context']).to be_nil - expect(json['orderedItems'][0]).to include_create_item(status) - expect(json['orderedItems'][1]).to include_create_item(private_status) - end - end - - def expect_likes_export - json = export_json(:likes) - - aggregate_failures do - expect(json['type']).to eq 'OrderedCollection' - expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(favourite.status)] - end - end - - def expect_bookmarks_export - json = export_json(:bookmarks) - - aggregate_failures do - expect(json['type']).to eq 'OrderedCollection' - expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(bookmark.status)] - end - end - - def export_json_raw(type) - read_zip_file(backup, "#{type}.json") - end - - def export_json(type) - Oj.load(export_json_raw(type)) - end - - def include_create_item(status) - include({ - 'type' => 'Create', - 'object' => include({ - 'id' => ActivityPub::TagManager.instance.uri_for(status), - 'content' => "

#{status.text}

", - }), - }) - end -end diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb deleted file mode 100644 index 628bb198ef..0000000000 --- a/spec/services/batched_remove_status_service_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe BatchedRemoveStatusService, :inline_jobs do - subject { described_class.new } - - let!(:alice) { Fabricate(:account) } - let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } - let!(:jeff) { Fabricate(:account) } - let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } - - let(:status_alice_hello) { PostStatusService.new.call(alice, text: "Hello @#{bob.pretty_acct}") } - let(:status_alice_other) { PostStatusService.new.call(alice, text: 'Another status') } - - before do - allow(redis).to receive_messages(publish: nil) - - stub_request(:post, 'http://example.com/inbox').to_return(status: 200) - - jeff.user.update(current_sign_in_at: Time.zone.now) - jeff.follow!(alice) - hank.follow!(alice) - - status_alice_hello - status_alice_other - - subject.call([status_alice_hello, status_alice_other]) - end - - it 'removes statuses' do - expect { Status.find(status_alice_hello.id) }.to raise_error ActiveRecord::RecordNotFound - expect { Status.find(status_alice_other.id) }.to raise_error ActiveRecord::RecordNotFound - end - - it 'removes statuses from author\'s home feed' do - expect(HomeFeed.new(alice).get(10).pluck(:id)).to_not include(status_alice_hello.id, status_alice_other.id) - end - - it 'removes statuses from local follower\'s home feed' do - expect(HomeFeed.new(jeff).get(10).pluck(:id)).to_not include(status_alice_hello.id, status_alice_other.id) - end - - it 'notifies streaming API of followers' do - expect(redis).to have_received(:publish).with("timeline:#{jeff.id}", any_args).at_least(:once) - end - - it 'notifies streaming API of public timeline' do - expect(redis).to have_received(:publish).with('timeline:public', any_args).at_least(:once) - end - - it 'sends delete activity to followers' do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.at_least_once - end -end diff --git a/spec/services/block_domain_service_spec.rb b/spec/services/block_domain_service_spec.rb deleted file mode 100644 index 839137db44..0000000000 --- a/spec/services/block_domain_service_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe BlockDomainService do - subject { described_class.new } - - let(:local_account) { Fabricate(:account) } - let(:bystander) { Fabricate(:account, domain: 'evil.org') } - let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') } - let!(:bad_status_plain) { Fabricate(:status, account: bad_account, text: 'You suck') } - let!(:bad_status_with_attachment) { Fabricate(:status, account: bad_account, text: 'Hahaha') } - let!(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status_with_attachment, file: attachment_fixture('attachment.jpg')) } - let!(:already_banned_account) { Fabricate(:account, username: 'badguy', domain: 'evil.org', suspended: true, silenced: true) } - - describe 'for a suspension' do - before do - local_account.follow!(bad_account) - bystander.follow!(local_account) - end - - it 'creates a domain block, suspends remote accounts with appropriate suspension date, records severed relationships and sends notification', :aggregate_failures do - subject.call(DomainBlock.create!(domain: 'evil.org', severity: :suspend)) - - expect(DomainBlock.blocked?('evil.org')).to be true - - # Suspends account with appropriate suspension date - expect(bad_account.reload.suspended?).to be true - expect(bad_account.reload.suspended_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at - - # Keep already-suspended account without updating the suspension date - expect(already_banned_account.reload.suspended?).to be true - expect(already_banned_account.reload.suspended_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at - - # Removes content - expect { bad_status_plain.reload }.to raise_exception ActiveRecord::RecordNotFound - expect { bad_status_with_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound - expect { bad_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound - - # Records severed relationships - severed_relationships = local_account.severed_relationships.to_a - expect(severed_relationships.count).to eq 2 - expect(severed_relationships[0].relationship_severance_event).to eq severed_relationships[1].relationship_severance_event - expect(severed_relationships.map { |rel| [rel.account, rel.target_account] }).to contain_exactly([bystander, local_account], [local_account, bad_account]) - - # Sends severed relationships notification - expect(LocalNotificationWorker).to have_enqueued_sidekiq_job(local_account.id, anything, 'AccountRelationshipSeveranceEvent', 'severed_relationships') - end - end - - describe 'for a silence with reject media' do - it 'does not mark the domain as blocked, but silences accounts with an appropriate silencing date, clears media', :aggregate_failures, :inline_jobs do - subject.call(DomainBlock.create!(domain: 'evil.org', severity: :silence, reject_media: true)) - - expect(DomainBlock.blocked?('evil.org')).to be false - - # Silences account with appropriate silecing date - expect(bad_account.reload.silenced?).to be true - expect(bad_account.reload.silenced_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at - - # Keeps already-silenced accounts without updating the silecing date - expect(already_banned_account.reload.silenced?).to be true - expect(already_banned_account.reload.silenced_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at - - # Leaves posts but clears media - expect { bad_status_plain.reload }.to_not raise_error - expect { bad_status_with_attachment.reload }.to_not raise_error - expect { bad_attachment.reload }.to_not raise_error - expect(bad_attachment.file.exists?).to be false - end - end -end diff --git a/spec/services/block_service_spec.rb b/spec/services/block_service_spec.rb deleted file mode 100644 index 46dd691986..0000000000 --- a/spec/services/block_service_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe BlockService do - subject { described_class.new } - - let(:sender) { Fabricate(:account, username: 'alice') } - - describe 'local' do - let(:bob) { Fabricate(:account, username: 'bob') } - - before do - NotificationPermission.create!(account: sender, from_account: bob) - end - - it 'creates a blocking relation and removes notification permissions' do - expect { subject.call(sender, bob) } - .to change { sender.blocking?(bob) }.from(false).to(true) - .and change { NotificationPermission.exists?(account: sender, from_account: bob) }.from(true).to(false) - end - end - - describe 'remote ActivityPub' do - let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } - - before do - stub_request(:post, 'http://example.com/inbox').to_return(status: 200) - subject.call(sender, bob) - end - - it 'creates a blocking relation' do - expect(sender.blocking?(bob)).to be true - end - - it 'sends a block activity', :inline_jobs do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once - end - end -end diff --git a/spec/services/bootstrap_timeline_service_spec.rb b/spec/services/bootstrap_timeline_service_spec.rb deleted file mode 100644 index c99813bceb..0000000000 --- a/spec/services/bootstrap_timeline_service_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe BootstrapTimelineService do - subject { described_class.new } - - context 'when the new user has registered from an invite' do - let(:service) { instance_double(FollowService) } - let(:autofollow) { false } - let(:inviter) { Fabricate(:user, confirmed_at: 2.days.ago) } - let(:invite) { Fabricate(:invite, user: inviter, max_uses: nil, expires_at: 1.hour.from_now, autofollow: autofollow) } - let(:new_user) { Fabricate(:user, invite_code: invite.code) } - - before do - allow(FollowService).to receive(:new).and_return(service) - allow(service).to receive(:call) - end - - context 'when the invite has auto-follow enabled' do - let(:autofollow) { true } - - it 'calls FollowService to follow the inviter' do - subject.call(new_user.account) - expect(service).to have_received(:call).with(new_user.account, inviter.account) - end - end - - context 'when the invite does not have auto-follow enable' do - let(:autofollow) { false } - - it 'calls FollowService to follow the inviter' do - subject.call(new_user.account) - expect(service).to_not have_received(:call) - end - end - end -end diff --git a/spec/services/bulk_import_row_service_spec.rb b/spec/services/bulk_import_row_service_spec.rb deleted file mode 100644 index b9af795a5d..0000000000 --- a/spec/services/bulk_import_row_service_spec.rb +++ /dev/null @@ -1,174 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe BulkImportRowService do - subject { described_class.new } - - let(:account) { Fabricate(:account) } - let(:import) { Fabricate(:bulk_import, account: account, type: import_type) } - let(:import_row) { Fabricate(:bulk_import_row, bulk_import: import, data: data) } - - describe '#call' do - context 'when importing a follow' do - let(:import_type) { 'following' } - let(:target_account) { Fabricate(:account) } - let(:service_double) { instance_double(FollowService, call: nil) } - let(:data) do - { 'acct' => target_account.acct } - end - - before do - allow(FollowService).to receive(:new).and_return(service_double) - end - - it 'calls FollowService with the expected arguments and returns true' do - expect(subject.call(import_row)).to be true - - expect(service_double).to have_received(:call).with(account, target_account, { reblogs: nil, notify: nil, languages: nil }) - end - end - - context 'when importing a block' do - let(:import_type) { 'blocking' } - let(:target_account) { Fabricate(:account) } - let(:service_double) { instance_double(BlockService, call: nil) } - let(:data) do - { 'acct' => target_account.acct } - end - - before do - allow(BlockService).to receive(:new).and_return(service_double) - end - - it 'calls BlockService with the expected arguments and returns true' do - expect(subject.call(import_row)).to be true - - expect(service_double).to have_received(:call).with(account, target_account) - end - end - - context 'when importing a mute' do - let(:import_type) { 'muting' } - let(:target_account) { Fabricate(:account) } - let(:service_double) { instance_double(MuteService, call: nil) } - let(:data) do - { 'acct' => target_account.acct } - end - - before do - allow(MuteService).to receive(:new).and_return(service_double) - end - - it 'calls MuteService with the expected arguments and returns true' do - expect(subject.call(import_row)).to be true - - expect(service_double).to have_received(:call).with(account, target_account, { notifications: nil }) - end - end - - context 'when importing a bookmark' do - let(:import_type) { 'bookmarks' } - let(:data) do - { 'uri' => ActivityPub::TagManager.instance.uri_for(target_status) } - end - - context 'when the status is public' do - let(:target_status) { Fabricate(:status) } - - it 'bookmarks the status and returns true' do - expect(subject.call(import_row)).to be true - expect(account.bookmarked?(target_status)).to be true - end - end - - context 'when the status is not accessible to the user' do - let(:target_status) { Fabricate(:status, visibility: :direct) } - - it 'does not bookmark the status and returns false' do - expect(subject.call(import_row)).to be false - expect(account.bookmarked?(target_status)).to be false - end - end - end - - context 'when importing a list row' do - let(:import_type) { 'lists' } - let(:target_account) { Fabricate(:account) } - let(:list_name) { 'my list' } - let(:data) do - { 'acct' => target_account.acct, 'list_name' => list_name } - end - - shared_examples 'common behavior' do - shared_examples 'row import success and list addition' do - it 'returns true and adds the target account to the list' do - result = nil - expect { result = subject.call(import_row) } - .to change { result }.from(nil).to(true) - .and add_target_account_to_list - end - end - - context 'when the target account is already followed' do - before do - account.follow!(target_account) - end - - include_examples 'row import success and list addition' - end - - context 'when the user already requested to follow the target account' do - before do - account.request_follow!(target_account) - end - - include_examples 'row import success and list addition' - end - - context 'when the target account is neither followed nor requested' do - include_examples 'row import success and list addition' - end - - context 'when the target account is the user themself' do - let(:target_account) { account } - - include_examples 'row import success and list addition' - end - - def add_target_account_to_list - change { target_account_on_list? } - .from(false) - .to(true) - end - - def target_account_on_list? - ListAccount - .joins(:list) - .exists?( - account_id: target_account.id, - list: { title: list_name } - ) - end - end - - context 'when the list does not exist yet' do - include_examples 'common behavior' - end - - context 'when the list exists' do - before do - Fabricate(:list, account: account, title: list_name) - end - - include_examples 'common behavior' - - it 'does not create a new list' do - account.follow!(target_account) - - expect { subject.call(import_row) }.to_not(change { List.where(title: list_name).count }) - end - end - end - end -end diff --git a/spec/services/bulk_import_service_spec.rb b/spec/services/bulk_import_service_spec.rb deleted file mode 100644 index e8bec96c85..0000000000 --- a/spec/services/bulk_import_service_spec.rb +++ /dev/null @@ -1,412 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe BulkImportService do - subject { described_class.new } - - let(:account) { Fabricate(:account) } - let(:import) { Fabricate(:bulk_import, account: account, type: import_type, overwrite: overwrite, state: :in_progress, imported_items: 0, processed_items: 0) } - - before do - import.update(total_items: import.rows.count) - end - - describe '#call' do - context 'when importing follows' do - let(:import_type) { 'following' } - let(:overwrite) { false } - - let!(:rows) do - [ - { 'acct' => 'user@foo.bar' }, - { 'acct' => 'unknown@unknown.bar' }, - ].map { |data| import.rows.create!(data: data) } - end - - before do - account.follow!(Fabricate(:account)) - end - - it 'does not immediately change who the account follows' do - expect { subject.call(import) }.to_not(change { account.reload.active_relationships.to_a }) - end - - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) - end - - it 'requests to follow all the listed users once the workers have run' do - subject.call(import) - - resolve_account_service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) - allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } - allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } - - Import::RowWorker.drain - - expect(FollowRequest.includes(:target_account).where(account: account).map { |follow_request| follow_request.target_account.acct }).to contain_exactly('user@foo.bar', 'unknown@unknown.bar') - end - end - - context 'when importing follows with overwrite' do - let(:import_type) { 'following' } - let(:overwrite) { true } - - let!(:followed) { Fabricate(:account, username: 'followed', domain: 'foo.bar', protocol: :activitypub) } - let!(:to_be_unfollowed) { Fabricate(:account, username: 'to_be_unfollowed', domain: 'foo.bar', protocol: :activitypub) } - - let!(:rows) do - [ - { 'acct' => 'followed@foo.bar', 'show_reblogs' => false, 'notify' => true, 'languages' => ['en'] }, - { 'acct' => 'user@foo.bar' }, - { 'acct' => 'unknown@unknown.bar' }, - ].map { |data| import.rows.create!(data: data) } - end - - before do - account.follow!(followed, reblogs: true, notify: false) - account.follow!(to_be_unfollowed) - end - - it 'unfollows user not present on list' do - subject.call(import) - expect(account.following?(to_be_unfollowed)).to be false - end - - it 'updates the existing follow relationship as expected' do - expect { subject.call(import) }.to change { Follow.where(account: account, target_account: followed).pick(:show_reblogs, :notify, :languages) }.from([true, false, nil]).to([false, true, ['en']]) - end - - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows[1..].map(&:id)) - end - - it 'requests to follow all the expected users once the workers have run' do - subject.call(import) - - resolve_account_service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) - allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } - allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } - - Import::RowWorker.drain - - expect(FollowRequest.includes(:target_account).where(account: account).map { |follow_request| follow_request.target_account.acct }).to contain_exactly('user@foo.bar', 'unknown@unknown.bar') - end - end - - context 'when importing blocks' do - let(:import_type) { 'blocking' } - let(:overwrite) { false } - - let!(:rows) do - [ - { 'acct' => 'user@foo.bar' }, - { 'acct' => 'unknown@unknown.bar' }, - ].map { |data| import.rows.create!(data: data) } - end - - before do - account.block!(Fabricate(:account, username: 'already_blocked', domain: 'remote.org')) - end - - it 'does not immediately change who the account blocks' do - expect { subject.call(import) }.to_not(change { account.reload.blocking.to_a }) - end - - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) - end - - it 'blocks all the listed users once the workers have run' do - subject.call(import) - - resolve_account_service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) - allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } - allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } - - Import::RowWorker.drain - - expect(account.blocking.map(&:acct)).to contain_exactly('already_blocked@remote.org', 'user@foo.bar', 'unknown@unknown.bar') - end - end - - context 'when importing blocks with overwrite' do - let(:import_type) { 'blocking' } - let(:overwrite) { true } - - let!(:blocked) { Fabricate(:account, username: 'blocked', domain: 'foo.bar', protocol: :activitypub) } - let!(:to_be_unblocked) { Fabricate(:account, username: 'to_be_unblocked', domain: 'foo.bar', protocol: :activitypub) } - - let!(:rows) do - [ - { 'acct' => 'blocked@foo.bar' }, - { 'acct' => 'user@foo.bar' }, - { 'acct' => 'unknown@unknown.bar' }, - ].map { |data| import.rows.create!(data: data) } - end - - before do - account.block!(blocked) - account.block!(to_be_unblocked) - end - - it 'unblocks user not present on list' do - subject.call(import) - expect(account.blocking?(to_be_unblocked)).to be false - end - - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows[1..].map(&:id)) - end - - it 'requests to follow all the expected users once the workers have run' do - subject.call(import) - - resolve_account_service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) - allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } - allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } - - Import::RowWorker.drain - - expect(account.blocking.map(&:acct)).to contain_exactly('blocked@foo.bar', 'user@foo.bar', 'unknown@unknown.bar') - end - end - - context 'when importing mutes' do - let(:import_type) { 'muting' } - let(:overwrite) { false } - - let!(:rows) do - [ - { 'acct' => 'user@foo.bar' }, - { 'acct' => 'unknown@unknown.bar' }, - ].map { |data| import.rows.create!(data: data) } - end - - before do - account.mute!(Fabricate(:account, username: 'already_muted', domain: 'remote.org')) - end - - it 'does not immediately change who the account blocks' do - expect { subject.call(import) }.to_not(change { account.reload.muting.to_a }) - end - - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) - end - - it 'mutes all the listed users once the workers have run' do - subject.call(import) - - resolve_account_service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) - allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } - allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } - - Import::RowWorker.drain - - expect(account.muting.map(&:acct)).to contain_exactly('already_muted@remote.org', 'user@foo.bar', 'unknown@unknown.bar') - end - end - - context 'when importing mutes with overwrite' do - let(:import_type) { 'muting' } - let(:overwrite) { true } - - let!(:muted) { Fabricate(:account, username: 'muted', domain: 'foo.bar', protocol: :activitypub) } - let!(:to_be_unmuted) { Fabricate(:account, username: 'to_be_unmuted', domain: 'foo.bar', protocol: :activitypub) } - - let!(:rows) do - [ - { 'acct' => 'muted@foo.bar', 'hide_notifications' => true }, - { 'acct' => 'user@foo.bar' }, - { 'acct' => 'unknown@unknown.bar' }, - ].map { |data| import.rows.create!(data: data) } - end - - before do - account.mute!(muted, notifications: false) - account.mute!(to_be_unmuted) - end - - it 'updates the existing mute as expected' do - expect { subject.call(import) }.to change { Mute.where(account: account, target_account: muted).pick(:hide_notifications) }.from(false).to(true) - end - - it 'unblocks user not present on list' do - subject.call(import) - expect(account.muting?(to_be_unmuted)).to be false - end - - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows[1..].map(&:id)) - end - - it 'requests to follow all the expected users once the workers have run' do - subject.call(import) - - resolve_account_service_double = instance_double(ResolveAccountService) - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service_double) - allow(resolve_account_service_double).to receive(:call).with('user@foo.bar', any_args) { Fabricate(:account, username: 'user', domain: 'foo.bar', protocol: :activitypub) } - allow(resolve_account_service_double).to receive(:call).with('unknown@unknown.bar', any_args) { Fabricate(:account, username: 'unknown', domain: 'unknown.bar', protocol: :activitypub) } - - Import::RowWorker.drain - - expect(account.muting.map(&:acct)).to contain_exactly('muted@foo.bar', 'user@foo.bar', 'unknown@unknown.bar') - end - end - - context 'when importing domain blocks' do - let(:import_type) { 'domain_blocking' } - let(:overwrite) { false } - - let(:rows) do - [ - { 'domain' => 'blocked.com' }, - { 'domain' => 'to_block.com' }, - ] - end - - before do - rows.each { |data| import.rows.create!(data: data) } - account.block_domain!('alreadyblocked.com') - account.block_domain!('blocked.com') - end - - it 'blocks all the new domains' do - subject.call(import) - expect(account.domain_blocks.pluck(:domain)).to contain_exactly('alreadyblocked.com', 'blocked.com', 'to_block.com') - end - - it 'marks the import as finished' do - subject.call(import) - expect(import.reload.state_finished?).to be true - end - end - - context 'when importing domain blocks with overwrite' do - let(:import_type) { 'domain_blocking' } - let(:overwrite) { true } - - let(:rows) do - [ - { 'domain' => 'blocked.com' }, - { 'domain' => 'to_block.com' }, - ] - end - - before do - rows.each { |data| import.rows.create!(data: data) } - account.block_domain!('alreadyblocked.com') - account.block_domain!('blocked.com') - end - - it 'blocks all the new domains' do - subject.call(import) - expect(account.domain_blocks.pluck(:domain)).to contain_exactly('blocked.com', 'to_block.com') - end - - it 'marks the import as finished' do - subject.call(import) - expect(import.reload.state_finished?).to be true - end - end - - context 'when importing bookmarks' do - let(:import_type) { 'bookmarks' } - let(:overwrite) { false } - - let!(:already_bookmarked) { Fabricate(:status, uri: 'https://already.bookmarked/1') } - let!(:status) { Fabricate(:status, uri: 'https://foo.bar/posts/1') } - let!(:inaccessible_status) { Fabricate(:status, uri: 'https://foo.bar/posts/inaccessible', visibility: :direct) } - let!(:bookmarked) { Fabricate(:status, uri: 'https://foo.bar/posts/already-bookmarked') } - - let!(:rows) do - [ - { 'uri' => status.uri }, - { 'uri' => inaccessible_status.uri }, - { 'uri' => bookmarked.uri }, - { 'uri' => 'https://domain.unknown/foo' }, - { 'uri' => 'https://domain.unknown/private' }, - ].map { |data| import.rows.create!(data: data) } - end - - before do - account.bookmarks.create!(status: already_bookmarked) - account.bookmarks.create!(status: bookmarked) - end - - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) - end - - it 'updates the bookmarks as expected once the workers have run' do - subject.call(import) - - service_double = instance_double(ActivityPub::FetchRemoteStatusService) - allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_double) - allow(service_double).to receive(:call).with('https://domain.unknown/foo') { Fabricate(:status, uri: 'https://domain.unknown/foo') } - allow(service_double).to receive(:call).with('https://domain.unknown/private') { Fabricate(:status, uri: 'https://domain.unknown/private', visibility: :direct) } - - Import::RowWorker.drain - - expect(account.bookmarks.map { |bookmark| bookmark.status.uri }).to contain_exactly(already_bookmarked.uri, status.uri, bookmarked.uri, 'https://domain.unknown/foo') - end - end - - context 'when importing bookmarks with overwrite' do - let(:import_type) { 'bookmarks' } - let(:overwrite) { true } - - let!(:already_bookmarked) { Fabricate(:status, uri: 'https://already.bookmarked/1') } - let!(:status) { Fabricate(:status, uri: 'https://foo.bar/posts/1') } - let!(:inaccessible_status) { Fabricate(:status, uri: 'https://foo.bar/posts/inaccessible', visibility: :direct) } - let!(:bookmarked) { Fabricate(:status, uri: 'https://foo.bar/posts/already-bookmarked') } - - let!(:rows) do - [ - { 'uri' => status.uri }, - { 'uri' => inaccessible_status.uri }, - { 'uri' => bookmarked.uri }, - { 'uri' => 'https://domain.unknown/foo' }, - { 'uri' => 'https://domain.unknown/private' }, - ].map { |data| import.rows.create!(data: data) } - end - - before do - account.bookmarks.create!(status: already_bookmarked) - account.bookmarks.create!(status: bookmarked) - end - - it 'enqueues workers for the expected rows' do - subject.call(import) - expect(Import::RowWorker.jobs.pluck('args').flatten).to match_array(rows.map(&:id)) - end - - it 'updates the bookmarks as expected once the workers have run' do - subject.call(import) - - service_double = instance_double(ActivityPub::FetchRemoteStatusService) - allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_double) - allow(service_double).to receive(:call).with('https://domain.unknown/foo') { Fabricate(:status, uri: 'https://domain.unknown/foo') } - allow(service_double).to receive(:call).with('https://domain.unknown/private') { Fabricate(:status, uri: 'https://domain.unknown/private', visibility: :direct) } - - Import::RowWorker.drain - - expect(account.bookmarks.map { |bookmark| bookmark.status.uri }).to contain_exactly(status.uri, bookmarked.uri, 'https://domain.unknown/foo') - end - end - end -end diff --git a/spec/services/clear_domain_media_service_spec.rb b/spec/services/clear_domain_media_service_spec.rb deleted file mode 100644 index f1e5097a99..0000000000 --- a/spec/services/clear_domain_media_service_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ClearDomainMediaService do - subject { described_class.new } - - let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') } - let!(:bad_status_plain) { Fabricate(:status, account: bad_account, text: 'You suck') } - let!(:bad_status_with_attachment) { Fabricate(:status, account: bad_account, text: 'Hahaha') } - let!(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status_with_attachment, file: attachment_fixture('attachment.jpg')) } - - describe 'for a silence with reject media' do - before do - subject.call(DomainBlock.create!(domain: 'evil.org', severity: :silence, reject_media: true)) - end - - it 'leaves the domains status and attachments, but clears media' do - expect { bad_status_plain.reload }.to_not raise_error - expect { bad_status_with_attachment.reload }.to_not raise_error - expect { bad_attachment.reload }.to_not raise_error - expect(bad_attachment.file.exists?).to be false - end - end -end diff --git a/spec/services/create_featured_tag_service_spec.rb b/spec/services/create_featured_tag_service_spec.rb deleted file mode 100644 index f057bc8538..0000000000 --- a/spec/services/create_featured_tag_service_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe CreateFeaturedTagService do - describe '#call' do - let(:tag) { 'test' } - - context 'with a local account' do - let(:account) { Fabricate(:account, domain: nil) } - - it 'creates a new featured tag and distributes' do - expect { subject.call(account, tag) } - .to change(FeaturedTag, :count).by(1) - expect(ActivityPub::AccountRawDistributionWorker) - .to have_enqueued_sidekiq_job(anything, account.id) - end - end - - context 'with a remote account' do - let(:account) { Fabricate(:account, domain: 'host.example') } - - it 'creates a new featured tag and does not distributes' do - expect { subject.call(account, tag) } - .to change(FeaturedTag, :count).by(1) - expect(ActivityPub::AccountRawDistributionWorker) - .to_not have_enqueued_sidekiq_job(any_args) - end - end - end -end diff --git a/spec/services/delete_account_service_spec.rb b/spec/services/delete_account_service_spec.rb deleted file mode 100644 index 741ac340cf..0000000000 --- a/spec/services/delete_account_service_spec.rb +++ /dev/null @@ -1,129 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe DeleteAccountService do - shared_examples 'common behavior' do - subject { described_class.new.call(account) } - - let!(:status) { Fabricate(:status, account: account) } - let!(:mention) { Fabricate(:mention, account: local_follower) } - let!(:status_with_mention) { Fabricate(:status, account: account, mentions: [mention]) } - let!(:media_attachment) { Fabricate(:media_attachment, account: account) } - let!(:notification) { Fabricate(:notification, account: account) } - let!(:favourite) { Fabricate(:favourite, account: account, status: Fabricate(:status, account: local_follower)) } - let!(:poll) { Fabricate(:poll, account: account) } - let!(:poll_vote) { Fabricate(:poll_vote, account: local_follower, poll: poll) } - - let!(:active_relationship) { Fabricate(:follow, account: account, target_account: local_follower) } - let!(:passive_relationship) { Fabricate(:follow, account: local_follower, target_account: account) } - let!(:endorsement) { Fabricate(:account_pin, account: local_follower, target_account: account) } - - let!(:mention_notification) { Fabricate(:notification, account: local_follower, activity: mention, type: :mention) } - let!(:status_notification) { Fabricate(:notification, account: local_follower, activity: status, type: :status) } - let!(:poll_notification) { Fabricate(:notification, account: local_follower, activity: poll, type: :poll) } - let!(:favourite_notification) { Fabricate(:notification, account: local_follower, activity: favourite, type: :favourite) } - let!(:follow_notification) { Fabricate(:notification, account: local_follower, activity: active_relationship, type: :follow) } - - let!(:account_note) { Fabricate(:account_note, account: account) } - - it 'deletes associated owned and target records and target notifications' do - subject - - expect_deletion_of_associated_owned_records - expect_deletion_of_associated_target_records - expect_deletion_of_associated_target_notifications - end - - def expect_deletion_of_associated_owned_records - expect { status.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { status_with_mention.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { mention.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { media_attachment.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { favourite.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { active_relationship.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { passive_relationship.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { poll.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { poll_vote.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { account_note.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - def expect_deletion_of_associated_target_records - expect { endorsement.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - def expect_deletion_of_associated_target_notifications - expect { favourite_notification.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { follow_notification.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { mention_notification.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { poll_notification.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { status_notification.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - describe '#call on local account', :inline_jobs do - before do - stub_request(:post, remote_alice.inbox_url).to_return(status: 201) - stub_request(:post, remote_bob.inbox_url).to_return(status: 201) - end - - let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', domain: 'alice.com', protocol: :activitypub) } - let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', domain: 'bob.com', protocol: :activitypub) } - - include_examples 'common behavior' do - let(:account) { Fabricate(:account) } - let(:local_follower) { Fabricate(:account) } - - it 'sends a delete actor activity to all known inboxes' do - subject - expect(a_request(:post, remote_alice.inbox_url)).to have_been_made.once - expect(a_request(:post, remote_bob.inbox_url)).to have_been_made.once - end - end - end - - describe '#call on remote account', :inline_jobs do - before do - stub_request(:post, account.inbox_url).to_return(status: 201) - end - - include_examples 'common behavior' do - let(:account) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub, domain: 'bob.com') } - let(:local_follower) { Fabricate(:account) } - - it 'sends expected activities to followed and follower inboxes' do - subject - - expect(post_to_inbox_with_reject).to have_been_made.once - expect(post_to_inbox_with_undo).to have_been_made.once - end - - def post_to_inbox_with_undo - a_request(:post, account.inbox_url).with( - body: hash_including({ - 'type' => 'Undo', - 'object' => hash_including({ - 'type' => 'Follow', - 'actor' => ActivityPub::TagManager.instance.uri_for(local_follower), - 'object' => account.uri, - }), - }) - ) - end - - def post_to_inbox_with_reject - a_request(:post, account.inbox_url).with( - body: hash_including({ - 'type' => 'Reject', - 'object' => hash_including({ - 'type' => 'Follow', - 'actor' => account.uri, - 'object' => ActivityPub::TagManager.instance.uri_for(local_follower), - }), - }) - ) - end - end - end -end diff --git a/spec/services/fan_out_on_write_service_spec.rb b/spec/services/fan_out_on_write_service_spec.rb deleted file mode 100644 index c6dd020cdf..0000000000 --- a/spec/services/fan_out_on_write_service_spec.rb +++ /dev/null @@ -1,117 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe FanOutOnWriteService do - subject { described_class.new } - - let(:last_active_at) { Time.now.utc } - let(:status) { Fabricate(:status, account: alice, visibility: visibility, text: 'Hello @bob @eve #hoge') } - - let!(:alice) { Fabricate(:user, current_sign_in_at: last_active_at).account } - let!(:bob) { Fabricate(:user, current_sign_in_at: last_active_at, account_attributes: { username: 'bob' }).account } - let!(:tom) { Fabricate(:user, current_sign_in_at: last_active_at).account } - let!(:eve) { Fabricate(:user, current_sign_in_at: last_active_at, account_attributes: { username: 'eve' }).account } - - before do - bob.follow!(alice) - tom.follow!(alice) - - ProcessMentionsService.new.call(status) - ProcessHashtagsService.new.call(status) - - Fabricate(:media_attachment, status: status, account: alice) - - allow(redis).to receive(:publish) - - subject.call(status) - end - - def home_feed_of(account) - HomeFeed.new(account).get(10).map(&:id) - end - - context 'when status is public' do - let(:visibility) { 'public' } - - it 'adds status to home feed of author and followers and broadcasts', :inline_jobs do - expect(status.id) - .to be_in(home_feed_of(alice)) - .and be_in(home_feed_of(bob)) - .and be_in(home_feed_of(tom)) - - expect(redis).to have_received(:publish).with('timeline:hashtag:hoge', anything) - expect(redis).to have_received(:publish).with('timeline:hashtag:hoge:local', anything) - expect(redis).to have_received(:publish).with('timeline:public', anything) - expect(redis).to have_received(:publish).with('timeline:public:local', anything) - expect(redis).to have_received(:publish).with('timeline:public:media', anything) - end - end - - context 'when status is limited' do - let(:visibility) { 'limited' } - - it 'adds status to home feed of author and mentioned followers and does not broadcast', :inline_jobs do - expect(status.id) - .to be_in(home_feed_of(alice)) - .and be_in(home_feed_of(bob)) - expect(status.id) - .to_not be_in(home_feed_of(tom)) - - expect_no_broadcasting - end - end - - context 'when status is private' do - let(:visibility) { 'private' } - - it 'adds status to home feed of author and followers and does not broadcast', :inline_jobs do - expect(status.id) - .to be_in(home_feed_of(alice)) - .and be_in(home_feed_of(bob)) - .and be_in(home_feed_of(tom)) - - expect_no_broadcasting - end - end - - context 'when status is direct' do - let(:visibility) { 'direct' } - - it 'is added to the home feed of its author and mentioned followers and does not broadcast', :inline_jobs do - expect(status.id) - .to be_in(home_feed_of(alice)) - .and be_in(home_feed_of(bob)) - expect(status.id) - .to_not be_in(home_feed_of(tom)) - - expect_no_broadcasting - end - - context 'when handling status updates' do - before do - subject.call(status) - - status.snapshot!(at_time: status.created_at, rate_limit: false) - status.update!(text: 'Hello @bob @eve #hoge (edited)') - status.snapshot!(account_id: status.account_id) - - redis.set("subscribed:timeline:#{eve.id}:notifications", '1') - end - - it 'pushes the update to mentioned users through the notifications streaming channel' do - subject.call(status, update: true) - expect(PushUpdateWorker).to have_enqueued_sidekiq_job(anything, status.id, "timeline:#{eve.id}:notifications", { 'update' => true }) - end - end - end - - def expect_no_broadcasting - expect(redis) - .to_not have_received(:publish) - .with('timeline:hashtag:hoge', anything) - expect(redis) - .to_not have_received(:publish) - .with('timeline:public', anything) - end -end diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb deleted file mode 100644 index c39362def2..0000000000 --- a/spec/services/favourite_service_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe FavouriteService do - subject { described_class.new } - - let(:sender) { Fabricate(:account, username: 'alice') } - - describe 'local' do - let(:bob) { Fabricate(:account) } - let(:status) { Fabricate(:status, account: bob) } - - before do - subject.call(sender, status) - end - - it 'creates a favourite' do - expect(status.favourites.first).to_not be_nil - end - end - - describe 'remote ActivityPub' do - let(:bob) { Fabricate(:account, protocol: :activitypub, username: 'bob', domain: 'example.com', inbox_url: 'http://example.com/inbox') } - let(:status) { Fabricate(:status, account: bob) } - - before do - stub_request(:post, 'http://example.com/inbox').to_return(status: 200, body: '', headers: {}) - subject.call(sender, status) - end - - it 'creates a favourite' do - expect(status.favourites.first).to_not be_nil - end - - it 'sends a like activity', :inline_jobs do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once - end - end -end diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb deleted file mode 100644 index 2f64f40558..0000000000 --- a/spec/services/fetch_link_card_service_spec.rb +++ /dev/null @@ -1,324 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe FetchLinkCardService do - subject { described_class.new } - - let(:html) { 'Hello world' } - let(:oembed_cache) { nil } - - before do - stub_request(:get, 'http://example.com/html').to_return(headers: { 'Content-Type' => 'text/html' }, body: html) - stub_request(:get, 'http://example.com/not-found').to_return(status: 404, headers: { 'Content-Type' => 'text/html' }, body: html) - stub_request(:get, 'http://example.com/text').to_return(status: 404, headers: { 'Content-Type' => 'text/plain' }, body: 'Hello') - stub_request(:get, 'http://example.com/redirect').to_return(status: 302, headers: { 'Location' => 'http://example.com/html' }) - stub_request(:get, 'http://example.com/redirect-to-404').to_return(status: 302, headers: { 'Location' => 'http://example.com/not-found' }) - stub_request(:get, 'http://example.com/oembed?url=http://example.com/html').to_return(headers: { 'Content-Type' => 'application/json' }, body: '{ "version": "1.0", "type": "link", "title": "oEmbed title" }') - stub_request(:get, 'http://example.com/oembed?format=json&url=http://example.com/html').to_return(headers: { 'Content-Type' => 'application/json' }, body: '{ "version": "1.0", "type": "link", "title": "oEmbed title" }') - - stub_request(:get, 'http://example.xn--fiqs8s') - stub_request(:get, 'http://example.com/日本語') - stub_request(:get, 'http://example.com/test?data=file.gpx%5E1') - stub_request(:get, 'http://example.com/test-') - - stub_request(:get, 'http://example.com/sjis').to_return(request_fixture('sjis.txt')) - stub_request(:get, 'http://example.com/sjis_with_wrong_charset').to_return(request_fixture('sjis_with_wrong_charset.txt')) - stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt')) - stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt')) - stub_request(:get, 'http://example.com/low_confidence_latin1').to_return(request_fixture('low_confidence_latin1.txt')) - stub_request(:get, 'http://example.com/latin1_posing_as_utf8_broken').to_return(request_fixture('latin1_posing_as_utf8_broken.txt')) - stub_request(:get, 'http://example.com/latin1_posing_as_utf8_recoverable').to_return(request_fixture('latin1_posing_as_utf8_recoverable.txt')) - stub_request(:get, 'http://example.com/aergerliche-umlaute').to_return(request_fixture('redirect_with_utf8_url.txt')) - stub_request(:get, 'http://example.com/page_without_title').to_return(request_fixture('page_without_title.txt')) - stub_request(:get, 'http://example.com/long_canonical_url').to_return(request_fixture('long_canonical_url.txt')) - stub_request(:get, 'http://example.com/alternative_utf8_spelling_in_header').to_return(request_fixture('alternative_utf8_spelling_in_header.txt')) - - Rails.cache.write('oembed_endpoint:example.com', oembed_cache) if oembed_cache - - subject.call(status) - end - - context 'with a local status' do - context 'with URL of a regular HTML page' do - let(:status) { Fabricate(:status, text: 'http://example.com/html') } - - it 'creates preview card' do - expect(status.preview_card).to_not be_nil - expect(status.preview_card.url).to eq 'http://example.com/html' - expect(status.preview_card.title).to eq 'Hello world' - end - end - - context 'with URL of a page with no title' do - let(:status) { Fabricate(:status, text: 'http://example.com/html') } - let(:html) { '' } - - it 'does not create a preview card' do - expect(status.preview_card).to be_nil - end - end - - context 'with a URL of a plain-text page' do - let(:status) { Fabricate(:status, text: 'http://example.com/text') } - - it 'does not create a preview card' do - expect(status.preview_card).to be_nil - end - end - - context 'with multiple URLs' do - let(:status) { Fabricate(:status, text: 'ftp://example.com http://example.com/html http://example.com/text') } - - it 'fetches the first valid URL' do - expect(a_request(:get, 'http://example.com/html')).to have_been_made - end - - it 'does not fetch the second valid URL' do - expect(a_request(:get, 'http://example.com/text/')).to_not have_been_made - end - end - - context 'with a redirect URL' do - let(:status) { Fabricate(:status, text: 'http://example.com/redirect') } - - it 'follows redirect' do - expect(a_request(:get, 'http://example.com/redirect')).to have_been_made.once - expect(a_request(:get, 'http://example.com/html')).to have_been_made.once - end - - it 'creates preview card' do - expect(status.preview_card).to_not be_nil - expect(status.preview_card.url).to eq 'http://example.com/html' - expect(status.preview_card.title).to eq 'Hello world' - end - end - - context 'with a broken redirect URL' do - let(:status) { Fabricate(:status, text: 'http://example.com/redirect-to-404') } - - it 'follows redirect' do - expect(a_request(:get, 'http://example.com/redirect-to-404')).to have_been_made.once - expect(a_request(:get, 'http://example.com/not-found')).to have_been_made.once - end - - it 'does not create a preview card' do - expect(status.preview_card).to be_nil - end - end - - context 'with a redirect URL with faulty encoding' do - let(:status) { Fabricate(:status, text: 'http://example.com/aergerliche-umlaute') } - - it 'does not create a preview card' do - expect(status.preview_card).to be_nil - end - end - - context 'with a page that has no title' do - let(:status) { Fabricate(:status, text: 'http://example.com/page_without_title') } - - it 'does not create a preview card' do - expect(status.preview_card).to be_nil - end - end - - context 'with a 404 URL' do - let(:status) { Fabricate(:status, text: 'http://example.com/not-found') } - - it 'does not create a preview card' do - expect(status.preview_card).to be_nil - end - end - - context 'with an IDN URL' do - let(:status) { Fabricate(:status, text: 'Check out http://example.中国') } - - it 'fetches the URL' do - expect(a_request(:get, 'http://example.xn--fiqs8s/')).to have_been_made.once - end - end - - context 'with a URL of a page in Shift JIS encoding' do - let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis') } - - it 'decodes the HTML' do - expect(status.preview_card.title).to eq('SJISのページ') - end - end - - context 'with a URL of a page in Shift JIS encoding labeled as UTF-8' do - let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis_with_wrong_charset') } - - it 'decodes the HTML despite the wrong charset header' do - expect(status.preview_card.title).to eq('SJISのページ') - end - end - - context 'with a URL of a page in KOI8-R encoding' do - let(:status) { Fabricate(:status, text: 'Check out http://example.com/koi8-r') } - - it 'decodes the HTML' do - expect(status.preview_card.title).to eq('Московя начинаетъ только въ XVI ст. привлекать внимане иностранцевъ.') - end - end - - context 'with a URL of a page in Windows-1251 encoding' do - let(:status) { Fabricate(:status, text: 'Check out http://example.com/windows-1251') } - - it 'decodes the HTML' do - expect(status.preview_card.title).to eq('сэмпл текст') - end - end - - context 'with a URL of a page in ISO-8859-1 encoding, that charlock_holmes cannot detect' do - context 'when encoding in http header is correct' do - let(:status) { Fabricate(:status, text: 'Check out http://example.com/low_confidence_latin1') } - - it 'decodes the HTML' do - expect(status.preview_card.title).to eq("Tofu á l'orange") - end - end - - context 'when encoding in http header is incorrect' do - context 'when encoding problems appear in unrelated tags' do - let(:status) { Fabricate(:status, text: 'Check out http://example.com/latin1_posing_as_utf8_recoverable') } - - it 'decodes the HTML' do - expect(status.preview_card.title).to eq('Tofu with orange sauce') - end - end - - context 'when encoding problems appear in title tag' do - let(:status) { Fabricate(:status, text: 'Check out http://example.com/latin1_posing_as_utf8_broken') } - - it 'does not create a preview card' do - expect(status.preview_card).to be_nil - end - end - end - end - - context 'with a Japanese path URL' do - let(:status) { Fabricate(:status, text: 'テストhttp://example.com/日本語') } - - it 'fetches the URL' do - expect(a_request(:get, 'http://example.com/日本語')).to have_been_made.once - end - end - - context 'with a hyphen-suffixed URL' do - let(:status) { Fabricate(:status, text: 'test http://example.com/test-') } - - it 'fetches the URL' do - expect(a_request(:get, 'http://example.com/test-')).to have_been_made.once - end - end - - context 'with a caret-suffixed URL' do - let(:status) { Fabricate(:status, text: 'test http://example.com/test?data=file.gpx^1') } - - it 'fetches the URL' do - expect(a_request(:get, 'http://example.com/test?data=file.gpx%5E1')).to have_been_made.once - end - - it 'does not strip the caret before fetching' do - expect(a_request(:get, 'http://example.com/test?data=file.gpx')).to_not have_been_made - end - end - - context 'with a non-isolated URL' do - let(:status) { Fabricate(:status, text: 'testhttp://example.com/sjis') } - - it 'does not fetch URLs not isolated from their surroundings' do - expect(a_request(:get, 'http://example.com/sjis')).to_not have_been_made - end - end - - context 'with a URL of a page with oEmbed support' do - let(:html) { 'Hello world' } - let(:status) { Fabricate(:status, text: 'http://example.com/html') } - - it 'fetches the oEmbed URL' do - expect(a_request(:get, 'http://example.com/oembed?url=http://example.com/html')).to have_been_made.once - end - - it 'creates preview card' do - expect(status.preview_card).to_not be_nil - expect(status.preview_card.url).to eq 'http://example.com/html' - expect(status.preview_card.title).to eq 'oEmbed title' - end - - context 'when oEmbed endpoint cache populated' do - let(:oembed_cache) { { endpoint: 'http://example.com/oembed?format=json&url={url}', format: :json } } - - it 'uses the cached oEmbed response' do - expect(a_request(:get, 'http://example.com/oembed?url=http://example.com/html')).to_not have_been_made - expect(a_request(:get, 'http://example.com/oembed?format=json&url=http://example.com/html')).to have_been_made - end - - it 'creates preview card' do - expect(status.preview_card).to_not be_nil - expect(status.preview_card.url).to eq 'http://example.com/html' - expect(status.preview_card.title).to eq 'oEmbed title' - end - end - - # If the original HTML URL for whatever reason (e.g. DOS protection) redirects to - # an error page, we can still use the cached oEmbed but should not use the - # redirect URL on the card. - context 'when oEmbed endpoint cache populated but page returns 404' do - let(:status) { Fabricate(:status, text: 'http://example.com/redirect-to-404') } - let(:oembed_cache) { { endpoint: 'http://example.com/oembed?url=http://example.com/html', format: :json } } - - it 'uses the cached oEmbed response' do - expect(a_request(:get, 'http://example.com/oembed?url=http://example.com/html')).to have_been_made - end - - it 'creates preview card' do - expect(status.preview_card).to_not be_nil - expect(status.preview_card.title).to eq 'oEmbed title' - end - - it 'uses the original URL' do - expect(status.preview_card&.url).to eq 'http://example.com/redirect-to-404' - end - end - end - - context 'with a URL of a page that includes a canonical URL too long for PostgreSQL unique indexes' do - let(:status) { Fabricate(:status, text: 'test http://example.com/long_canonical_url') } - - it 'does not create a preview card' do - expect(status.preview_card).to be_nil - end - end - - context 'with a URL where the `Content-Type` header uses `utf8` instead of `utf-8`' do - let(:status) { Fabricate(:status, text: 'test http://example.com/alternative_utf8_spelling_in_header') } - - it 'does not create a preview card' do - expect(status.preview_card.title).to eq 'Webserver Configs R Us' - end - end - end - - context 'with a remote status' do - let(:status) do - Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: <<-TEXT) - Habt ihr ein paar gute Links zu foo - #Wannacry herumfliegen? - Ich will mal unter
http://example.com/not-found was sammeln. ! - security  - TEXT - end - - it 'parses out URLs' do - expect(a_request(:get, 'http://example.com/not-found')).to have_been_made.once - end - - it 'ignores URLs to hashtags' do - expect(a_request(:get, 'https://quitter.se/tag/wannacry')).to_not have_been_made - end - end -end diff --git a/spec/services/fetch_oembed_service_spec.rb b/spec/services/fetch_oembed_service_spec.rb deleted file mode 100644 index c9f84048b6..0000000000 --- a/spec/services/fetch_oembed_service_spec.rb +++ /dev/null @@ -1,201 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe FetchOEmbedService do - subject { described_class.new } - - before do - stub_request(:get, 'https://host.test/provider.json').to_return(status: 404) - stub_request(:get, 'https://host.test/provider.xml').to_return(status: 404) - stub_request(:get, 'https://host.test/empty_provider.json').to_return(status: 200) - end - - describe 'discover_provider' do - context 'when status code is 200 and MIME type is text/html' do - context 'when OEmbed endpoint contains URL as parameter' do - before do - stub_request(:get, 'https://www.youtube.com/watch?v=IPSbNdBmWKE').to_return( - status: 200, - headers: { 'Content-Type': 'text/html' }, - body: request_fixture('oembed_youtube.html') - ) - stub_request(:get, 'https://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DIPSbNdBmWKE').to_return( - status: 200, - headers: { 'Content-Type': 'text/html' }, - body: request_fixture('oembed_json_empty.html') - ) - end - - it 'returns new OEmbed::Provider for JSON provider' do - subject.call('https://www.youtube.com/watch?v=IPSbNdBmWKE') - expect(subject.endpoint_url).to eq 'https://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DIPSbNdBmWKE' - expect(subject.format).to eq :json - end - - it 'stores URL template' do - subject.call('https://www.youtube.com/watch?v=IPSbNdBmWKE') - expect(Rails.cache.read('oembed_endpoint:www.youtube.com')[:endpoint]).to eq 'https://www.youtube.com/oembed?format=json&url={url}' - end - end - - context 'when both of JSON and XML provider are discoverable' do - before do - stub_request(:get, 'https://host.test/oembed.html').to_return( - status: 200, - headers: { 'Content-Type': 'text/html' }, - body: request_fixture('oembed_json_xml.html') - ) - end - - it 'returns new OEmbed::Provider for JSON provider if :format option is set to :json' do - subject.call('https://host.test/oembed.html', format: :json) - expect(subject.endpoint_url).to eq 'https://host.test/provider.json' - expect(subject.format).to eq :json - end - - it 'returns new OEmbed::Provider for XML provider if :format option is set to :xml' do - subject.call('https://host.test/oembed.html', format: :xml) - expect(subject.endpoint_url).to eq 'https://host.test/provider.xml' - expect(subject.format).to eq :xml - end - - it 'does not cache OEmbed endpoint' do - subject.call('https://host.test/oembed.html', format: :xml) - expect(Rails.cache.exist?('oembed_endpoint:host.test')).to be false - end - end - - context 'when JSON provider is discoverable while XML provider is not' do - before do - stub_request(:get, 'https://host.test/oembed.html').to_return( - status: 200, - headers: { 'Content-Type': 'text/html' }, - body: request_fixture('oembed_json.html') - ) - end - - it 'returns new OEmbed::Provider for JSON provider' do - subject.call('https://host.test/oembed.html') - expect(subject.endpoint_url).to eq 'https://host.test/provider.json' - expect(subject.format).to eq :json - end - - it 'does not cache OEmbed endpoint' do - subject.call('https://host.test/oembed.html') - expect(Rails.cache.exist?('oembed_endpoint:host.test')).to be false - end - end - - context 'when XML provider is discoverable while JSON provider is not' do - before do - stub_request(:get, 'https://host.test/oembed.html').to_return( - status: 200, - headers: { 'Content-Type': 'text/html' }, - body: request_fixture('oembed_xml.html') - ) - end - - it 'returns new OEmbed::Provider for XML provider' do - subject.call('https://host.test/oembed.html') - expect(subject.endpoint_url).to eq 'https://host.test/provider.xml' - expect(subject.format).to eq :xml - end - - it 'does not cache OEmbed endpoint' do - subject.call('https://host.test/oembed.html') - expect(Rails.cache.exist?('oembed_endpoint:host.test')).to be false - end - end - - context 'with Invalid XML provider is discoverable while JSON provider is not' do - before do - stub_request(:get, 'https://host.test/oembed.html').to_return( - status: 200, - headers: { 'Content-Type': 'text/html' }, - body: request_fixture('oembed_invalid_xml.html') - ) - end - - it 'returns nil' do - expect(subject.call('https://host.test/oembed.html')).to be_nil - end - end - - context 'with neither of JSON and XML provider is discoverable' do - before do - stub_request(:get, 'https://host.test/oembed.html').to_return( - status: 200, - headers: { 'Content-Type': 'text/html' }, - body: request_fixture('oembed_undiscoverable.html') - ) - end - - it 'returns nil' do - expect(subject.call('https://host.test/oembed.html')).to be_nil - end - end - - context 'when empty JSON provider is discoverable' do - before do - stub_request(:get, 'https://host.test/oembed.html').to_return( - status: 200, - headers: { 'Content-Type': 'text/html' }, - body: request_fixture('oembed_json_empty.html') - ) - end - - it 'returns new OEmbed::Provider for JSON provider' do - subject.call('https://host.test/oembed.html') - expect(subject.endpoint_url).to eq 'https://host.test/empty_provider.json' - expect(subject.format).to eq :json - end - end - end - - context 'when endpoint is cached' do - before do - stub_request(:get, 'http://www.youtube.com/oembed?format=json&url=https://www.youtube.com/watch?v=dqwpQarrDwk').to_return( - status: 200, - headers: { 'Content-Type': 'text/html' }, - body: request_fixture('oembed_json_empty.html') - ) - end - - it 'returns new provider without fetching original URL first' do - subject.call('https://www.youtube.com/watch?v=dqwpQarrDwk', cached_endpoint: { endpoint: 'http://www.youtube.com/oembed?format=json&url={url}', format: :json }) - expect(a_request(:get, 'https://www.youtube.com/watch?v=dqwpQarrDwk')).to_not have_been_made - expect(subject.endpoint_url).to eq 'http://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdqwpQarrDwk' - expect(subject.format).to eq :json - expect(a_request(:get, 'http://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdqwpQarrDwk')).to have_been_made - end - end - - context 'when status code is not 200' do - before do - stub_request(:get, 'https://host.test/oembed.html').to_return( - status: 400, - headers: { 'Content-Type': 'text/html' }, - body: request_fixture('oembed_xml.html') - ) - end - - it 'returns nil' do - expect(subject.call('https://host.test/oembed.html')).to be_nil - end - end - - context 'when MIME type is not text/html' do - before do - stub_request(:get, 'https://host.test/oembed.html').to_return( - status: 200, - body: request_fixture('oembed_xml.html') - ) - end - - it 'returns nil' do - expect(subject.call('https://host.test/oembed.html')).to be_nil - end - end - end -end diff --git a/spec/services/fetch_remote_status_service_spec.rb b/spec/services/fetch_remote_status_service_spec.rb deleted file mode 100644 index a9c61e7b4e..0000000000 --- a/spec/services/fetch_remote_status_service_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe FetchRemoteStatusService do - let(:account) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/foo') } - let(:prefetched_body) { nil } - - let(:note) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://example.org/@foo/1234', - type: 'Note', - content: 'Lorem ipsum', - attributedTo: ActivityPub::TagManager.instance.uri_for(account), - } - end - - context 'when protocol is :activitypub' do - subject { described_class.new.call(note[:id], prefetched_body: prefetched_body) } - - let(:prefetched_body) { Oj.dump(note) } - - before do - subject - end - - it 'creates status' do - status = account.statuses.first - - expect(status).to_not be_nil - expect(status.text).to eq 'Lorem ipsum' - end - end -end diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb deleted file mode 100644 index ee4810571b..0000000000 --- a/spec/services/fetch_resource_service_spec.rb +++ /dev/null @@ -1,110 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe FetchResourceService do - describe '#call' do - subject { described_class.new.call(url) } - - let(:url) { 'http://example.com' } - - context 'with blank url' do - let(:url) { '' } - - it { is_expected.to be_nil } - end - - context 'when request fails' do - before do - stub_request(:get, url).to_return(status: 500, body: '', headers: {}) - end - - it { is_expected.to be_nil } - end - - context 'when OpenSSL::SSL::SSLError is raised' do - before do - request = instance_double(Request) - allow(Request).to receive(:new).and_return(request) - allow(request).to receive(:add_headers) - allow(request).to receive(:on_behalf_of) - allow(request).to receive(:perform).and_raise(OpenSSL::SSL::SSLError) - end - - it { is_expected.to be_nil } - end - - context 'when HTTP::ConnectionError is raised' do - before do - request = instance_double(Request) - allow(Request).to receive(:new).and_return(request) - allow(request).to receive(:add_headers) - allow(request).to receive(:on_behalf_of) - allow(request).to receive(:perform).and_raise(HTTP::ConnectionError) - end - - it { is_expected.to be_nil } - end - - context 'when request succeeds' do - let(:body) { '' } - - let(:content_type) { 'application/json' } - - let(:headers) do - { 'Content-Type' => content_type } - end - - let(:json) do - { - id: 'http://example.com/foo', - '@context': ActivityPub::TagManager::CONTEXT, - type: 'Note', - }.to_json - end - - before do - stub_request(:get, url).to_return(status: 200, body: body, headers: headers) - stub_request(:get, 'http://example.com/foo').to_return(status: 200, body: json, headers: { 'Content-Type' => 'application/activity+json' }) - end - - it 'signs request' do - subject - expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.key_uri_for(Account.representative))}"/ })).to have_been_made - end - - context 'when content type is application/atom+xml' do - let(:content_type) { 'application/atom+xml' } - - it { is_expected.to be_nil } - end - - context 'when content type is activity+json' do - let(:content_type) { 'application/activity+json; charset=utf-8' } - let(:body) { json } - - it { is_expected.to eq ['http://example.com/foo', { prefetched_body: body }] } - end - - context 'when content type is ld+json with profile' do - let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } - let(:body) { json } - - it { is_expected.to eq ['http://example.com/foo', { prefetched_body: body }] } - end - - context 'when link header is present' do - let(:headers) { { 'Link' => '; rel="alternate"; type="application/activity+json"' } } - - it { is_expected.to eq ['http://example.com/foo', { prefetched_body: json }] } - end - - context 'when content type is text/html' do - let(:content_type) { 'text/html' } - let(:body) { '' } - - it { is_expected.to eq ['http://example.com/foo', { prefetched_body: json }] } - end - end - end -end diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb deleted file mode 100644 index 0c4cd60046..0000000000 --- a/spec/services/follow_service_spec.rb +++ /dev/null @@ -1,157 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe FollowService do - subject { described_class.new } - - let(:sender) { Fabricate(:account, username: 'alice') } - - context 'when local account' do - describe 'locked account' do - let(:bob) { Fabricate(:account, locked: true, username: 'bob') } - - before do - subject.call(sender, bob) - end - - it 'creates a follow request with reblogs' do - expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: true)).to_not be_nil - end - end - - describe 'locked account, no reblogs' do - let(:bob) { Fabricate(:account, locked: true, username: 'bob') } - - before do - subject.call(sender, bob, reblogs: false) - end - - it 'creates a follow request without reblogs' do - expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: false)).to_not be_nil - end - end - - describe 'unlocked account, from silenced account' do - let(:bob) { Fabricate(:account, username: 'bob') } - - before do - sender.touch(:silenced_at) - subject.call(sender, bob) - end - - it 'creates a follow request with reblogs' do - expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: true)).to_not be_nil - end - end - - describe 'unlocked account, from a muted account' do - let(:bob) { Fabricate(:account, username: 'bob') } - - before do - bob.mute!(sender) - subject.call(sender, bob) - end - - it 'creates a following relation with reblogs' do - expect(sender.following?(bob)).to be true - expect(sender.muting_reblogs?(bob)).to be false - end - end - - describe 'unlocked account' do - let(:bob) { Fabricate(:account, username: 'bob') } - - before do - subject.call(sender, bob) - end - - it 'creates a following relation with reblogs' do - expect(sender.following?(bob)).to be true - expect(sender.muting_reblogs?(bob)).to be false - end - end - - describe 'unlocked account, no reblogs' do - let(:bob) { Fabricate(:account, username: 'bob') } - - before do - subject.call(sender, bob, reblogs: false) - end - - it 'creates a following relation without reblogs' do - expect(sender.following?(bob)).to be true - expect(sender.muting_reblogs?(bob)).to be true - end - end - - describe 'already followed account' do - let(:bob) { Fabricate(:account, username: 'bob') } - - before do - sender.follow!(bob) - subject.call(sender, bob) - end - - it 'keeps a following relation' do - expect(sender.following?(bob)).to be true - end - end - - describe 'already followed account, turning reblogs off' do - let(:bob) { Fabricate(:account, username: 'bob') } - - before do - sender.follow!(bob, reblogs: true) - subject.call(sender, bob, reblogs: false) - end - - it 'disables reblogs' do - expect(sender.muting_reblogs?(bob)).to be true - end - end - - describe 'already followed account, turning reblogs on' do - let(:bob) { Fabricate(:account, username: 'bob') } - - before do - sender.follow!(bob, reblogs: false) - subject.call(sender, bob, reblogs: true) - end - - it 'disables reblogs' do - expect(sender.muting_reblogs?(bob)).to be false - end - end - - describe 'already followed account, changing languages' do - let(:bob) { Fabricate(:account, username: 'bob') } - - before do - sender.follow!(bob) - subject.call(sender, bob, languages: %w(en es)) - end - - it 'changes languages' do - expect(Follow.find_by(account: sender, target_account: bob)&.languages).to match_array %w(en es) - end - end - end - - context 'when remote ActivityPub account' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } - - before do - stub_request(:post, 'http://example.com/inbox').to_return(status: 200, body: '', headers: {}) - subject.call(sender, bob) - end - - it 'creates follow request' do - expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil - end - - it 'sends a follow activity to the inbox', :inline_jobs do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once - end - end -end diff --git a/spec/services/import_service_spec.rb b/spec/services/import_service_spec.rb deleted file mode 100644 index 0a99c5e748..0000000000 --- a/spec/services/import_service_spec.rb +++ /dev/null @@ -1,242 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ImportService, :inline_jobs do - include RoutingHelper - - let!(:account) { Fabricate(:account, locked: false) } - let!(:bob) { Fabricate(:account, username: 'bob', locked: false) } - let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false, protocol: :activitypub, inbox_url: 'https://example.com/inbox') } - - before do - stub_request(:post, 'https://example.com/inbox').to_return(status: 200) - end - - context 'when importing old-style list of muted users' do - subject { described_class.new } - - let(:csv) { attachment_fixture('mute-imports.txt') } - - describe 'when no accounts are muted' do - let(:import) { Import.create(account: account, type: 'muting', data: csv) } - - it 'mutes the listed accounts, including notifications' do - subject.call(import) - expect(account.muting.count).to eq 2 - expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true - end - end - - describe 'when some accounts are muted and overwrite is not set' do - let(:import) { Import.create(account: account, type: 'muting', data: csv) } - - it 'mutes the listed accounts, including notifications' do - account.mute!(bob, notifications: false) - subject.call(import) - expect(account.muting.count).to eq 2 - expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true - end - end - - describe 'when some accounts are muted and overwrite is set' do - let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) } - - it 'mutes the listed accounts, including notifications' do - account.mute!(bob, notifications: false) - subject.call(import) - expect(account.muting.count).to eq 2 - expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true - end - end - end - - context 'when importing new-style list of muted users' do - subject { described_class.new } - - let(:csv) { attachment_fixture('new-mute-imports.txt') } - - describe 'when no accounts are muted' do - let(:import) { Import.create(account: account, type: 'muting', data: csv) } - - it 'mutes the listed accounts, respecting notifications' do - subject.call(import) - expect(account.muting.count).to eq 2 - expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true - expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false - end - end - - describe 'when some accounts are muted and overwrite is not set' do - let(:import) { Import.create(account: account, type: 'muting', data: csv) } - - it 'mutes the listed accounts, respecting notifications' do - account.mute!(bob, notifications: true) - subject.call(import) - expect(account.muting.count).to eq 2 - expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true - expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false - end - end - - describe 'when some accounts are muted and overwrite is set' do - let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) } - - it 'mutes the listed accounts, respecting notifications' do - account.mute!(bob, notifications: true) - subject.call(import) - expect(account.muting.count).to eq 2 - expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true - expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false - end - end - end - - context 'when importing old-style list of followed users' do - subject { described_class.new } - - let(:csv) { attachment_fixture('mute-imports.txt') } - - describe 'when no accounts are followed' do - let(:import) { Import.create(account: account, type: 'following', data: csv) } - - it 'follows the listed accounts, including boosts' do - subject.call(import) - - expect(account.following.count).to eq 1 - expect(account.follow_requests.count).to eq 1 - expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - end - end - - describe 'when some accounts are already followed and overwrite is not set' do - let(:import) { Import.create(account: account, type: 'following', data: csv) } - - it 'follows the listed accounts, including notifications' do - account.follow!(bob, reblogs: false) - subject.call(import) - expect(account.following.count).to eq 1 - expect(account.follow_requests.count).to eq 1 - expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - end - end - - describe 'when some accounts are already followed and overwrite is set' do - let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) } - - it 'mutes the listed accounts, including notifications' do - account.follow!(bob, reblogs: false) - subject.call(import) - expect(account.following.count).to eq 1 - expect(account.follow_requests.count).to eq 1 - expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - end - end - end - - context 'when importing new-style list of followed users' do - subject { described_class.new } - - let(:csv) { attachment_fixture('new-following-imports.txt') } - - describe 'when no accounts are followed' do - let(:import) { Import.create(account: account, type: 'following', data: csv) } - - it 'follows the listed accounts, respecting boosts' do - subject.call(import) - expect(account.following.count).to eq 1 - expect(account.follow_requests.count).to eq 1 - expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false - end - end - - describe 'when some accounts are already followed and overwrite is not set' do - let(:import) { Import.create(account: account, type: 'following', data: csv) } - - it 'mutes the listed accounts, respecting notifications' do - account.follow!(bob, reblogs: true) - subject.call(import) - expect(account.following.count).to eq 1 - expect(account.follow_requests.count).to eq 1 - expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false - end - end - - describe 'when some accounts are already followed and overwrite is set' do - let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) } - - it 'mutes the listed accounts, respecting notifications' do - account.follow!(bob, reblogs: true) - subject.call(import) - expect(account.following.count).to eq 1 - expect(account.follow_requests.count).to eq 1 - expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false - end - end - end - - # Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users - # - # https://github.com/mastodon/mastodon/issues/20571 - context 'with a utf-8 encoded domains' do - subject { described_class.new } - - let!(:nare) { Fabricate(:account, username: 'nare', domain: 'թութ.հայ', locked: false, protocol: :activitypub, inbox_url: 'https://թութ.հայ/inbox') } - let(:csv) { attachment_fixture('utf8-followers.txt') } - let(:import) { Import.create(account: account, type: 'following', data: csv) } - - # Make sure to not actually go to the remote server - before do - stub_request(:post, nare.inbox_url).to_return(status: 200) - end - - it 'follows the listed account' do - expect(account.follow_requests.count).to eq 0 - subject.call(import) - expect(account.follow_requests.count).to eq 1 - end - end - - context 'when importing bookmarks' do - subject { described_class.new } - - let(:csv) { attachment_fixture('bookmark-imports.txt') } - let(:local_account) { Fabricate(:account, username: 'foo', domain: '') } - let!(:remote_status) { Fabricate(:status, uri: 'https://example.com/statuses/1312') } - let!(:direct_status) { Fabricate(:status, uri: 'https://example.com/statuses/direct', visibility: :direct) } - - around do |example| - local_before = Rails.configuration.x.local_domain - web_before = Rails.configuration.x.web_domain - Rails.configuration.x.local_domain = 'local.com' - Rails.configuration.x.web_domain = 'local.com' - example.run - Rails.configuration.x.web_domain = web_before - Rails.configuration.x.local_domain = local_before - end - - before do - service = instance_double(ActivityPub::FetchRemoteStatusService) - allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service) - allow(service).to receive(:call).with('https://unknown-remote.com/users/bar/statuses/1') do - Fabricate(:status, uri: 'https://unknown-remote.com/users/bar/statuses/1') - end - end - - describe 'when no bookmarks are set' do - let(:import) { Import.create(account: account, type: 'bookmarks', data: csv) } - - it 'adds the toots the user has access to to bookmarks' do - local_status = Fabricate(:status, account: local_account, uri: 'https://local.com/users/foo/statuses/42', id: 42, local: true) - subject.call(import) - expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to include(local_status.id) - expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to include(remote_status.id) - expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to_not include(direct_status.id) - expect(account.bookmarks.count).to eq 3 - end - end - end -end diff --git a/spec/services/move_service_spec.rb b/spec/services/move_service_spec.rb deleted file mode 100644 index e63818f67e..0000000000 --- a/spec/services/move_service_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe MoveService do - subject { described_class.new.call(migration) } - - context 'with a valid migration record' do - let(:migration) { Fabricate(:account_migration, account: source_account, target_account: target_account) } - let(:source_account) { Fabricate(:account) } - let(:target_account) { Fabricate(:account, also_known_as: [source_account_uri]) } - - it 'migrates the account to a new account' do - expect { subject } - .to change_source_moved_value - .and process_local_updates - .and distribute_updates - .and distribute_move - end - end - - def source_account_uri - ActivityPub::TagManager - .instance - .uri_for(source_account) - end - - def change_source_moved_value - change(source_account.reload, :moved_to_account) - .from(nil) - .to(target_account) - end - - def process_local_updates - enqueue_sidekiq_job(MoveWorker) - .with(source_account.id, target_account.id) - end - - def distribute_updates - enqueue_sidekiq_job(ActivityPub::UpdateDistributionWorker) - .with(source_account.id) - end - - def distribute_move - enqueue_sidekiq_job(ActivityPub::MoveDistributionWorker) - .with(migration.id) - end -end diff --git a/spec/services/mute_service_spec.rb b/spec/services/mute_service_spec.rb deleted file mode 100644 index 3bde92b87a..0000000000 --- a/spec/services/mute_service_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe MuteService do - subject { described_class.new.call(account, target_account) } - - let(:account) { Fabricate(:account) } - let(:target_account) { Fabricate(:account) } - - describe 'home timeline' do - let(:status) { Fabricate(:status, account: target_account) } - let(:other_account_status) { Fabricate(:status) } - let(:home_timeline_key) { FeedManager.instance.key(:home, account.id) } - - before do - redis.del(home_timeline_key) - end - - it "clears account's statuses", :inline_jobs do - FeedManager.instance.push_to_home(account, status) - FeedManager.instance.push_to_home(account, other_account_status) - - expect { subject }.to change { - redis.zrange(home_timeline_key, 0, -1) - }.from([status.id.to_s, other_account_status.id.to_s]).to([other_account_status.id.to_s]) - end - end - - it 'mutes account' do - expect { subject }.to change { - account.muting?(target_account) - }.from(false).to(true) - end - - context 'without specifying a notifications parameter' do - it 'mutes notifications from the account' do - expect { subject }.to change { - account.muting_notifications?(target_account) - }.from(false).to(true) - end - end - - context 'with a true notifications parameter' do - subject { described_class.new.call(account, target_account, notifications: true) } - - it 'mutes notifications from the account' do - expect { subject }.to change { - account.muting_notifications?(target_account) - }.from(false).to(true) - end - end - - context 'with a false notifications parameter' do - subject { described_class.new.call(account, target_account, notifications: false) } - - it 'does not mute notifications from the account' do - expect { subject }.to_not change { - account.muting_notifications?(target_account) - }.from(false) - end - end -end diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb deleted file mode 100644 index c7e00129b2..0000000000 --- a/spec/services/notify_service_spec.rb +++ /dev/null @@ -1,391 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe NotifyService do - subject { described_class.new.call(recipient, type, activity) } - - let(:user) { Fabricate(:user) } - let(:recipient) { user.account } - let(:sender) { Fabricate(:account, domain: 'example.com') } - let(:activity) { Fabricate(:follow, account: sender, target_account: recipient) } - let(:type) { :follow } - - it { expect { subject }.to change(Notification, :count).by(1) } - - it 'does not notify when sender is blocked' do - recipient.block!(sender) - expect { subject }.to_not change(Notification, :count) - end - - it 'does not notify when sender is muted with hide_notifications' do - recipient.mute!(sender, notifications: true) - expect { subject }.to_not change(Notification, :count) - end - - it 'does notify when sender is muted without hide_notifications' do - recipient.mute!(sender, notifications: false) - expect { subject }.to change(Notification, :count) - end - - it 'does not notify when sender\'s domain is blocked' do - recipient.block_domain!(sender.domain) - expect { subject }.to_not change(Notification, :count) - end - - it 'does still notify when sender\'s domain is blocked but sender is followed' do - recipient.block_domain!(sender.domain) - recipient.follow!(sender) - expect { subject }.to change(Notification, :count) - end - - it 'does not notify when sender is silenced and not followed' do - sender.silence! - subject - expect(Notification.find_by(activity: activity).filtered?).to be true - end - - it 'does not notify when recipient is suspended' do - recipient.suspend! - expect { subject }.to_not change(Notification, :count) - end - - describe 'reblogs' do - let(:status) { Fabricate(:status, account: Fabricate(:account)) } - let(:activity) { Fabricate(:status, account: sender, reblog: status) } - let(:type) { :reblog } - - it 'shows reblogs by default' do - recipient.follow!(sender) - expect { subject }.to change(Notification, :count) - end - - it 'shows reblogs when explicitly enabled' do - recipient.follow!(sender, reblogs: true) - expect { subject }.to change(Notification, :count) - end - - it 'shows reblogs when disabled' do - recipient.follow!(sender, reblogs: false) - expect { subject }.to change(Notification, :count) - end - end - - context 'with muted and blocked users' do - let(:asshole) { Fabricate(:account, username: 'asshole') } - let(:reply_to) { Fabricate(:status, account: asshole) } - let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, thread: reply_to)) } - let(:type) { :mention } - - it 'does not notify when conversation is muted' do - recipient.mute_conversation!(activity.status.conversation) - expect { subject }.to_not change(Notification, :count) - end - - it 'does not notify when it is a reply to a blocked user' do - recipient.block!(asshole) - expect { subject }.to_not change(Notification, :count) - end - end - - context 'with sender as recipient' do - let(:sender) { recipient } - - it 'does not notify when recipient is the sender' do - expect { subject }.to_not change(Notification, :count) - end - end - - describe 'email' do - before do - user.settings.update('notification_emails.follow': enabled) - user.save - end - - context 'when email notification is enabled' do - let(:enabled) { true } - - it 'sends email', :inline_jobs do - emails = capture_emails { subject } - - expect(emails.size) - .to eq(1) - expect(emails.first) - .to have_attributes( - to: contain_exactly(user.email), - subject: eq(I18n.t('notification_mailer.follow.subject', name: sender.acct)) - ) - end - end - - context 'when email notification is disabled' do - let(:enabled) { false } - - it "doesn't send email" do - emails = capture_emails { subject } - - expect(emails).to be_empty - end - end - end - - context 'with filtered notifications' do - let(:unknown) { Fabricate(:account, username: 'unknown') } - let(:status) { Fabricate(:status, account: unknown) } - let(:activity) { Fabricate(:mention, account: recipient, status: status) } - let(:type) { :mention } - - before do - Fabricate(:notification_policy, account: recipient, filter_not_following: true) - end - - it 'creates a filtered notification' do - expect { subject }.to change(Notification, :count) - expect(Notification.last).to be_filtered - end - - context 'when no notification request exists' do - it 'creates a notification request' do - expect { subject }.to change(NotificationRequest, :count) - end - end - - context 'when a notification request exists' do - let!(:notification_request) do - Fabricate(:notification_request, account: recipient, from_account: unknown, last_status: Fabricate(:status, account: unknown)) - end - - it 'updates the existing notification request' do - expect { subject }.to_not change(NotificationRequest, :count) - expect(notification_request.reload.last_status).to eq status - end - end - end - - describe NotifyService::DismissCondition do - subject { described_class.new(notification) } - - let(:activity) { Fabricate(:mention, status: Fabricate(:status)) } - let(:notification) { Fabricate(:notification, type: :mention, activity: activity, from_account: activity.status.account, account: activity.account) } - - describe '#dismiss?' do - context 'when sender is silenced' do - before do - notification.from_account.silence! - end - - it 'returns false' do - expect(subject.dismiss?).to be false - end - end - - context 'when recipient has blocked sender' do - before do - notification.account.block!(notification.from_account) - end - - it 'returns true' do - expect(subject.dismiss?).to be true - end - end - end - end - - describe NotifyService::FilterCondition do - subject { described_class.new(notification) } - - let(:activity) { Fabricate(:mention, status: Fabricate(:status)) } - let(:notification) { Fabricate(:notification, type: :mention, activity: activity, from_account: activity.status.account, account: activity.account) } - - describe '#filter?' do - context 'when sender is silenced' do - before do - notification.from_account.silence! - end - - it 'returns true' do - expect(subject.filter?).to be true - end - - context 'when recipient follows sender' do - before do - notification.account.follow!(notification.from_account) - end - - it 'returns false' do - expect(subject.filter?).to be false - end - end - end - - context 'when recipient is filtering not-followed senders' do - before do - Fabricate(:notification_policy, account: notification.account, filter_not_following: true) - end - - it 'returns true' do - expect(subject.filter?).to be true - end - - context 'when sender has permission' do - before do - Fabricate(:notification_permission, account: notification.account, from_account: notification.from_account) - end - - it 'returns false' do - expect(subject.filter?).to be false - end - end - - context 'when sender is followed by recipient' do - before do - notification.account.follow!(notification.from_account) - end - - it 'returns false' do - expect(subject.filter?).to be false - end - end - end - - context 'when recipient is filtering not-followers' do - before do - Fabricate(:notification_policy, account: notification.account, filter_not_followers: true) - end - - it 'returns true' do - expect(subject.filter?).to be true - end - - context 'when sender has permission' do - before do - Fabricate(:notification_permission, account: notification.account, from_account: notification.from_account) - end - - it 'returns false' do - expect(subject.filter?).to be false - end - end - - context 'when sender follows recipient' do - before do - notification.from_account.follow!(notification.account) - end - - it 'returns true' do - expect(subject.filter?).to be true - end - end - - context 'when sender follows recipient for longer than 3 days' do - before do - follow = notification.from_account.follow!(notification.account) - follow.update(created_at: 4.days.ago) - end - - it 'returns false' do - expect(subject.filter?).to be false - end - end - end - - context 'when recipient is filtering new accounts' do - before do - Fabricate(:notification_policy, account: notification.account, filter_new_accounts: true) - end - - it 'returns true' do - expect(subject.filter?).to be true - end - - context 'when sender has permission' do - before do - Fabricate(:notification_permission, account: notification.account, from_account: notification.from_account) - end - - it 'returns false' do - expect(subject.filter?).to be false - end - end - - context 'when sender is older than 30 days' do - before do - notification.from_account.update(created_at: 31.days.ago) - end - - it 'returns false' do - expect(subject.filter?).to be false - end - end - end - - context 'when recipient is not filtering anyone' do - before do - Fabricate(:notification_policy, account: notification.account) - end - - it 'returns false' do - expect(subject.filter?).to be false - end - end - - context 'when recipient is filtering unsolicited private mentions' do - before do - Fabricate(:notification_policy, account: notification.account, filter_private_mentions: true) - end - - context 'when notification is not a private mention' do - it 'returns false' do - expect(subject.filter?).to be false - end - end - - context 'when notification is a private mention' do - before do - notification.target_status.update(visibility: :direct) - end - - it 'returns true' do - expect(subject.filter?).to be true - end - - context 'when the message chain is initiated by recipient, but sender is not mentioned' do - before do - original_status = Fabricate(:status, account: notification.account, visibility: :direct) - notification.target_status.update(thread: original_status) - end - - it 'returns true' do - expect(subject.filter?).to be true - end - end - - context 'when the message chain is initiated by recipient, and sender is mentioned' do - before do - original_status = Fabricate(:status, account: notification.account, visibility: :direct) - notification.target_status.update(thread: original_status) - Fabricate(:mention, status: original_status, account: notification.from_account) - end - - it 'returns false' do - expect(subject.filter?).to be false - end - end - - context 'when the sender is mentioned in an unrelated message chain' do - before do - original_status = Fabricate(:status, visibility: :direct) - intermediary_status = Fabricate(:status, visibility: :direct, thread: original_status) - notification.target_status.update(thread: intermediary_status) - Fabricate(:mention, status: original_status, account: notification.from_account) - end - - it 'returns true' do - expect(subject.filter?).to be true - end - end - end - end - end - end -end diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb deleted file mode 100644 index f21548b5f2..0000000000 --- a/spec/services/post_status_service_spec.rb +++ /dev/null @@ -1,289 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe PostStatusService do - subject { described_class.new } - - it 'creates a new status' do - account = Fabricate(:account) - text = 'test status update' - - status = subject.call(account, text: text) - - expect(status).to be_persisted - expect(status.text).to eq text - end - - it 'creates a new response status' do - in_reply_to_status = Fabricate(:status) - account = Fabricate(:account) - text = 'test status update' - - status = subject.call(account, text: text, thread: in_reply_to_status) - - expect(status).to be_persisted - expect(status.text).to eq text - expect(status.thread).to eq in_reply_to_status - end - - context 'when scheduling a status' do - let!(:account) { Fabricate(:account) } - let!(:future) { Time.now.utc + 2.hours } - let!(:previous_status) { Fabricate(:status, account: account) } - - it 'schedules a status for future creation and does not create one immediately' do - media = Fabricate(:media_attachment, account: account) - status = subject.call(account, text: 'Hi future!', media_ids: [media.id], scheduled_at: future) - - expect(status) - .to be_a(ScheduledStatus) - .and have_attributes( - scheduled_at: eq(future), - params: include( - 'text' => eq('Hi future!'), - 'media_ids' => contain_exactly(media.id) - ) - ) - expect(media.reload.status).to be_nil - expect(Status.where(text: 'Hi future!')).to_not exist - end - - it 'does not change statuses_count of account or replies_count of thread previous status' do - expect { subject.call(account, text: 'Hi future!', scheduled_at: future, thread: previous_status) } - .to not_change { account.statuses_count } - .and(not_change { previous_status.replies_count }) - end - - it 'returns existing status when used twice with idempotency key' do - account = Fabricate(:account) - status1 = subject.call(account, text: 'test', idempotency: 'meepmeep', scheduled_at: future) - status2 = subject.call(account, text: 'test', idempotency: 'meepmeep', scheduled_at: future) - expect(status2.id).to eq status1.id - end - - context 'when scheduled_at is less than min offset' do - let(:invalid_scheduled_time) { 4.minutes.from_now } - - it 'raises invalid record error' do - expect do - subject.call(account, text: 'Hi future!', scheduled_at: invalid_scheduled_time) - end.to raise_error(ActiveRecord::RecordInvalid) - end - end - end - - it 'creates response to the original status of boost' do - boosted_status = Fabricate(:status) - in_reply_to_status = Fabricate(:status, reblog: boosted_status) - account = Fabricate(:account) - text = 'test status update' - - status = subject.call(account, text: text, thread: in_reply_to_status) - - expect(status).to be_persisted - expect(status.text).to eq text - expect(status.thread).to eq boosted_status - end - - it 'creates a sensitive status' do - status = create_status_with_options(sensitive: true) - - expect(status).to be_persisted - expect(status).to be_sensitive - end - - it 'creates a status with spoiler text' do - spoiler_text = 'spoiler text' - - status = create_status_with_options(spoiler_text: spoiler_text) - - expect(status).to be_persisted - expect(status.spoiler_text).to eq spoiler_text - end - - it 'creates a sensitive status when there is a CW but no text' do - status = subject.call(Fabricate(:account), text: '', spoiler_text: 'foo') - - expect(status).to be_persisted - expect(status).to be_sensitive - end - - it 'creates a status with empty default spoiler text' do - status = create_status_with_options(spoiler_text: nil) - - expect(status).to be_persisted - expect(status.spoiler_text).to eq '' - end - - it 'creates a status with the given visibility' do - status = create_status_with_options(visibility: :private) - - expect(status).to be_persisted - expect(status.visibility).to eq 'private' - end - - it 'creates a status with limited visibility for silenced users' do - status = subject.call(Fabricate(:account, silenced: true), text: 'test', visibility: :public) - - expect(status).to be_persisted - expect(status.visibility).to eq 'unlisted' - end - - it 'creates a status for the given application' do - application = Fabricate(:application) - - status = create_status_with_options(application: application) - - expect(status).to be_persisted - expect(status.application).to eq application - end - - it 'creates a status with a language set' do - account = Fabricate(:account) - text = 'This is an English text.' - - status = subject.call(account, text: text) - - expect(status.language).to eq 'en' - end - - it 'processes mentions' do - mention_service = instance_double(ProcessMentionsService) - allow(mention_service).to receive(:call) - allow(ProcessMentionsService).to receive(:new).and_return(mention_service) - account = Fabricate(:account) - - status = subject.call(account, text: 'test status update') - - expect(ProcessMentionsService).to have_received(:new) - expect(mention_service).to have_received(:call).with(status, save_records: false) - end - - it 'safeguards mentions' do - account = Fabricate(:account) - mentioned_account = Fabricate(:account, username: 'alice') - unexpected_mentioned_account = Fabricate(:account, username: 'bob') - - expect do - subject.call(account, text: '@alice hm, @bob is really annoying lately', allowed_mentions: [mentioned_account.id]) - end.to raise_error(an_instance_of(described_class::UnexpectedMentionsError).and(having_attributes(accounts: [unexpected_mentioned_account]))) - end - - it 'processes duplicate mentions correctly' do - account = Fabricate(:account) - Fabricate(:account, username: 'alice') - - expect do - subject.call(account, text: '@alice @alice @alice hey @alice') - end.to_not raise_error - end - - it 'processes hashtags' do - hashtags_service = instance_double(ProcessHashtagsService) - allow(hashtags_service).to receive(:call) - allow(ProcessHashtagsService).to receive(:new).and_return(hashtags_service) - account = Fabricate(:account) - - status = subject.call(account, text: 'test status update') - - expect(ProcessHashtagsService).to have_received(:new) - expect(hashtags_service).to have_received(:call).with(status) - end - - it 'gets distributed' do - allow(DistributionWorker).to receive(:perform_async) - allow(ActivityPub::DistributionWorker).to receive(:perform_async) - - account = Fabricate(:account) - - status = subject.call(account, text: 'test status update') - - expect(DistributionWorker).to have_received(:perform_async).with(status.id) - expect(ActivityPub::DistributionWorker).to have_received(:perform_async).with(status.id) - end - - it 'crawls links' do - allow(LinkCrawlWorker).to receive(:perform_async) - account = Fabricate(:account) - - status = subject.call(account, text: 'test status update') - - expect(LinkCrawlWorker).to have_received(:perform_async).with(status.id) - end - - it 'attaches the given media to the created status' do - account = Fabricate(:account) - media = Fabricate(:media_attachment, account: account) - - status = subject.call( - account, - text: 'test status update', - media_ids: [media.id] - ) - - expect(media.reload.status).to eq status - end - - it 'does not attach media from another account to the created status' do - account = Fabricate(:account) - media = Fabricate(:media_attachment, account: Fabricate(:account)) - - subject.call( - account, - text: 'test status update', - media_ids: [media.id] - ) - - expect(media.reload.status).to be_nil - end - - it 'does not allow attaching more files than configured limit' do - stub_const('Status::MEDIA_ATTACHMENTS_LIMIT', 1) - account = Fabricate(:account) - - expect do - subject.call( - account, - text: 'test status update', - media_ids: Array.new(2) { Fabricate(:media_attachment, account: account) }.map(&:id) - ) - end.to raise_error( - Mastodon::ValidationError, - I18n.t('media_attachments.validations.too_many') - ) - end - - it 'does not allow attaching both videos and images' do - account = Fabricate(:account) - video = Fabricate(:media_attachment, type: :video, account: account) - image = Fabricate(:media_attachment, type: :image, account: account) - - video.update(type: :video) - - expect do - subject.call( - account, - text: 'test status update', - media_ids: [ - video, - image, - ].map(&:id) - ) - end.to raise_error( - Mastodon::ValidationError, - I18n.t('media_attachments.validations.images_and_video') - ) - end - - it 'returns existing status when used twice with idempotency key' do - account = Fabricate(:account) - status1 = subject.call(account, text: 'test', idempotency: 'meepmeep') - status2 = subject.call(account, text: 'test', idempotency: 'meepmeep') - expect(status2.id).to eq status1.id - end - - def create_status_with_options(**options) - subject.call(Fabricate(:account), options.merge(text: 'test')) - end -end diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb deleted file mode 100644 index 9b2c6c280f..0000000000 --- a/spec/services/precompute_feed_service_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe PrecomputeFeedService do - subject { described_class.new } - - describe 'call' do - let(:account) { Fabricate(:account) } - - it 'fills a user timeline with statuses' do - account = Fabricate(:account) - status = Fabricate(:status, account: account) - - subject.call(account) - - expect(redis.zscore(FeedManager.instance.key(:home, account.id), status.id)).to be_within(0.1).of(status.id.to_f) - end - - it 'does not raise an error even if it could not find any status' do - account = Fabricate(:account) - expect { subject.call(account) }.to_not raise_error - end - - it 'filters statuses' do - account = Fabricate(:account) - muted_account = Fabricate(:account) - Fabricate(:mute, account: account, target_account: muted_account) - reblog = Fabricate(:status, account: muted_account) - Fabricate(:status, account: account, reblog: reblog) - - subject.call(account) - - expect(redis.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to be_nil - end - end -end diff --git a/spec/services/process_hashtags_service_spec.rb b/spec/services/process_hashtags_service_spec.rb deleted file mode 100644 index a0d5ef3464..0000000000 --- a/spec/services/process_hashtags_service_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ProcessHashtagsService do - describe '#call' do - let(:status) { Fabricate(:status, visibility: :public, text: 'With tags #one #two') } - - it 'applies the tags from the status text' do - expect { subject.call(status) } - .to change(Tag, :count).by(2) - expect(status.reload.tags.map(&:name)) - .to contain_exactly('one', 'two') - end - end -end diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb deleted file mode 100644 index 2c202d3e57..0000000000 --- a/spec/services/process_mentions_service_spec.rb +++ /dev/null @@ -1,106 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ProcessMentionsService do - subject { described_class.new } - - let(:account) { Fabricate(:account, username: 'alice') } - - context 'when mentions contain blocked accounts' do - let(:non_blocked_account) { Fabricate(:account) } - let(:individually_blocked_account) { Fabricate(:account) } - let(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com') } - let(:status) { Fabricate(:status, account: account, text: "Hello @#{non_blocked_account.acct} @#{individually_blocked_account.acct} @#{domain_blocked_account.acct}", visibility: :public) } - - before do - account.block!(individually_blocked_account) - account.domain_blocks.create!(domain: domain_blocked_account.domain) - - subject.call(status) - end - - it 'creates a mention to the non-blocked account' do - expect(non_blocked_account.mentions.where(status: status).count).to eq 1 - end - - it 'does not create a mention to the individually blocked account' do - expect(individually_blocked_account.mentions.where(status: status).count).to eq 0 - end - - it 'does not create a mention to the domain-blocked account' do - expect(domain_blocked_account.mentions.where(status: status).count).to eq 0 - end - end - - context 'with resolving a mention to a remote account' do - let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}", visibility: :public) } - - context 'with ActivityPub' do - context 'with a valid remote user' do - let!(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } - - before do - subject.call(status) - end - - it 'creates a mention' do - expect(remote_user.mentions.where(status: status).count).to eq 1 - end - end - - context 'when mentioning a user several times when not saving records' do - let!(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } - let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct} @#{remote_user.acct} @#{remote_user.acct}", visibility: :public) } - - before do - subject.call(status, save_records: false) - end - - it 'creates exactly one mention' do - expect(status.mentions.size).to eq 1 - end - end - - context 'with an IDN domain' do - let!(:remote_user) { Fabricate(:account, username: 'sneak', protocol: :activitypub, domain: 'xn--hresiar-mxa.ch', inbox_url: 'http://example.com/inbox') } - let!(:status) { Fabricate(:status, account: account, text: 'Hello @sneak@hæresiar.ch') } - - before do - subject.call(status) - end - - it 'creates a mention' do - expect(remote_user.mentions.where(status: status).count).to eq 1 - end - end - - context 'with an IDN TLD' do - let!(:remote_user) { Fabricate(:account, username: 'foo', protocol: :activitypub, domain: 'xn--y9a3aq.xn--y9a3aq', inbox_url: 'http://example.com/inbox') } - let!(:status) { Fabricate(:status, account: account, text: 'Hello @foo@հայ.հայ') } - - before do - subject.call(status) - end - - it 'creates a mention' do - expect(remote_user.mentions.where(status: status).count).to eq 1 - end - end - end - - context 'with a Temporarily-unreachable ActivityPub user' do - let!(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox', last_webfingered_at: nil) } - - before do - stub_request(:get, 'https://example.com/.well-known/host-meta').to_return(status: 404) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:remote_user@example.com').to_return(status: 500) - subject.call(status) - end - - it 'creates a mention' do - expect(remote_user.mentions.where(status: status).count).to eq 1 - end - end - end -end diff --git a/spec/services/purge_domain_service_spec.rb b/spec/services/purge_domain_service_spec.rb deleted file mode 100644 index a5c49160db..0000000000 --- a/spec/services/purge_domain_service_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe PurgeDomainService do - subject { described_class.new } - - let(:domain) { 'obsolete.org' } - let!(:account) { Fabricate(:account, domain: domain) } - let!(:status_plain) { Fabricate(:status, account: account) } - let!(:status_with_attachment) { Fabricate(:status, account: account) } - let!(:attachment) { Fabricate(:media_attachment, account: account, status: status_with_attachment, file: attachment_fixture('attachment.jpg')) } - - describe 'for a suspension' do - it 'refreshes instance view and removes associated records' do - expect { subject.call(domain) } - .to change { domain_instance_exists }.from(true).to(false) - - expect { account.reload }.to raise_exception ActiveRecord::RecordNotFound - expect { status_plain.reload }.to raise_exception ActiveRecord::RecordNotFound - expect { status_with_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound - expect { attachment.reload }.to raise_exception ActiveRecord::RecordNotFound - end - - def domain_instance_exists - Instance.exists?(domain: domain) - end - end -end diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb deleted file mode 100644 index f807536a2e..0000000000 --- a/spec/services/reblog_service_spec.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ReblogService do - let(:alice) { Fabricate(:account, username: 'alice') } - - context 'when creates a reblog with appropriate visibility' do - subject { described_class.new } - - let(:visibility) { :public } - let(:reblog_visibility) { :public } - let(:status) { Fabricate(:status, account: alice, visibility: visibility) } - - before do - subject.call(alice, status, visibility: reblog_visibility) - end - - describe 'boosting privately' do - let(:reblog_visibility) { :private } - - it 'reblogs privately' do - expect(status.reblogs.first.visibility).to eq 'private' - end - end - - describe 'public reblogs of private toots should remain private' do - let(:visibility) { :private } - let(:reblog_visibility) { :public } - - it 'reblogs privately' do - expect(status.reblogs.first.visibility).to eq 'private' - end - end - end - - context 'when the reblogged status is discarded in the meantime' do - let(:status) { Fabricate(:status, account: alice, visibility: :public, text: 'discard-status-text') } - - # Add a callback to discard the status being reblogged after the - # validations pass but before the database commit is executed. - before do - Status.class_eval do - before_save :discard_status - def discard_status - Status - .where(id: reblog_of_id) - .where(text: 'discard-status-text') - .update_all(deleted_at: Time.now.utc) - end - end - end - - # Remove race condition simulating `discard_status` callback. - after do - Status._save_callbacks.delete(:discard_status) - end - - it 'raises an exception' do - expect { subject.call(alice, status) }.to raise_error ActiveRecord::ActiveRecordError - end - end - - context 'with ActivityPub' do - subject { described_class.new } - - let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } - let(:status) { Fabricate(:status, account: bob) } - - before do - stub_request(:post, bob.inbox_url) - allow(ActivityPub::DistributionWorker).to receive(:perform_async) - subject.call(alice, status) - end - - it 'creates a reblog' do - expect(status.reblogs.count).to eq 1 - end - - describe 'after_create_commit :store_uri' do - it 'keeps consistent reblog count' do - expect(status.reblogs.count).to eq 1 - end - end - - it 'distributes to followers' do - expect(ActivityPub::DistributionWorker).to have_received(:perform_async) - end - end -end diff --git a/spec/services/reject_follow_service_spec.rb b/spec/services/reject_follow_service_spec.rb deleted file mode 100644 index d2c7a00206..0000000000 --- a/spec/services/reject_follow_service_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe RejectFollowService do - subject { described_class.new } - - let(:sender) { Fabricate(:account, username: 'alice') } - - describe 'local' do - let(:bob) { Fabricate(:account) } - - before do - FollowRequest.create(account: bob, target_account: sender) - subject.call(bob, sender) - end - - it 'removes follow request' do - expect(bob.requested?(sender)).to be false - end - - it 'does not create follow relation' do - expect(bob.following?(sender)).to be false - end - end - - describe 'remote ActivityPub' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } - - before do - FollowRequest.create(account: bob, target_account: sender) - stub_request(:post, bob.inbox_url).to_return(status: 200) - subject.call(bob, sender) - end - - it 'removes follow request' do - expect(bob.requested?(sender)).to be false - end - - it 'does not create follow relation' do - expect(bob.following?(sender)).to be false - end - - it 'sends a reject activity', :inline_jobs do - expect(a_request(:post, bob.inbox_url)).to have_been_made.once - end - end -end diff --git a/spec/services/remove_domains_from_followers_service_spec.rb b/spec/services/remove_domains_from_followers_service_spec.rb deleted file mode 100644 index 9e9d6cef2d..0000000000 --- a/spec/services/remove_domains_from_followers_service_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe RemoveDomainsFromFollowersService do - describe '#call' do - context 'with account followers' do - let(:account) { Fabricate(:account, domain: nil) } - let(:good_domain_account) { Fabricate(:account, domain: 'good.example', protocol: :activitypub) } - let(:bad_domain_account) { Fabricate(:account, domain: 'bad.example', protocol: :activitypub) } - - before do - Fabricate :follow, target_account: account, account: good_domain_account - Fabricate :follow, target_account: account, account: bad_domain_account - end - - it 'removes followers from supplied domains and sends a notification' do - subject.call(account, ['bad.example']) - - expect(account.followers) - .to include(good_domain_account) - .and not_include(bad_domain_account) - expect(ActivityPub::DeliveryWorker) - .to have_enqueued_sidekiq_job(anything, account.id, bad_domain_account.inbox_url) - end - end - end -end diff --git a/spec/services/remove_featured_tag_service_spec.rb b/spec/services/remove_featured_tag_service_spec.rb deleted file mode 100644 index 2f0694bc65..0000000000 --- a/spec/services/remove_featured_tag_service_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe RemoveFeaturedTagService do - describe '#call' do - context 'with a featured tag' do - let(:featured_tag) { Fabricate(:featured_tag) } - - context 'when called by a local account' do - let(:account) { Fabricate(:account, domain: nil) } - - it 'destroys the featured tag and sends a distribution' do - subject.call(account, featured_tag) - - expect { featured_tag.reload } - .to raise_error(ActiveRecord::RecordNotFound) - expect(ActivityPub::AccountRawDistributionWorker) - .to have_enqueued_sidekiq_job(anything, account.id) - end - end - - context 'when called by a non local account' do - let(:account) { Fabricate(:account, domain: 'host.example') } - - it 'destroys the featured tag and does not send a distribution' do - subject.call(account, featured_tag) - - expect { featured_tag.reload } - .to raise_error(ActiveRecord::RecordNotFound) - expect(ActivityPub::AccountRawDistributionWorker) - .to_not have_enqueued_sidekiq_job(any_args) - end - end - end - end -end diff --git a/spec/services/remove_from_followers_service_spec.rb b/spec/services/remove_from_followers_service_spec.rb deleted file mode 100644 index 515600096c..0000000000 --- a/spec/services/remove_from_followers_service_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe RemoveFromFollowersService do - subject { described_class.new } - - let(:bob) { Fabricate(:account, username: 'bob') } - - describe 'local' do - let(:sender) { Fabricate(:account, username: 'alice') } - - before do - Follow.create(account: sender, target_account: bob) - subject.call(bob, sender) - end - - it 'does not create follow relation' do - expect(bob.followed_by?(sender)).to be false - end - end - - describe 'remote ActivityPub' do - let(:sender) { Fabricate(:account, username: 'alice', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } - - before do - Follow.create(account: sender, target_account: bob) - stub_request(:post, sender.inbox_url).to_return(status: 200) - subject.call(bob, sender) - end - - it 'does not create follow relation' do - expect(bob.followed_by?(sender)).to be false - end - - it 'sends a reject activity', :inline_jobs do - expect(a_request(:post, sender.inbox_url)).to have_been_made.once - end - end -end diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb deleted file mode 100644 index 08f519b536..0000000000 --- a/spec/services/remove_status_service_spec.rb +++ /dev/null @@ -1,136 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe RemoveStatusService, :inline_jobs do - subject { described_class.new } - - let!(:alice) { Fabricate(:account) } - let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } - let!(:jeff) { Fabricate(:account) } - let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } - let!(:bill) { Fabricate(:account, username: 'bill', protocol: :activitypub, domain: 'example2.com', inbox_url: 'http://example2.com/inbox') } - - before do - stub_request(:post, hank.inbox_url).to_return(status: 200) - stub_request(:post, bill.inbox_url).to_return(status: 200) - - jeff.follow!(alice) - hank.follow!(alice) - end - - context 'when removed status is not a reblog' do - let!(:media_attachment) { Fabricate(:media_attachment, account: alice) } - let!(:status) { PostStatusService.new.call(alice, text: "Hello @#{bob.pretty_acct} ThisIsASecret", media_ids: [media_attachment.id]) } - - before do - FavouriteService.new.call(jeff, status) - Fabricate(:status, account: bill, reblog: status, uri: 'hoge') - end - - it 'removes status from author\'s home feed' do - subject.call(status) - expect(HomeFeed.new(alice).get(10).pluck(:id)).to_not include(status.id) - end - - it 'removes status from local follower\'s home feed' do - subject.call(status) - expect(HomeFeed.new(jeff).get(10).pluck(:id)).to_not include(status.id) - end - - it 'publishes to public media timeline' do - allow(redis).to receive(:publish).with(any_args) - - subject.call(status) - - expect(redis).to have_received(:publish).with('timeline:public:media', Oj.dump(event: :delete, payload: status.id.to_s)) - end - - it 'sends Delete activity to followers' do - subject.call(status) - - expect(delete_delivery(hank, status)) - .to have_been_made.once - end - - it 'sends Delete activity to rebloggers' do - subject.call(status) - - expect(delete_delivery(bill, status)) - .to have_been_made.once - end - - it 'remove status from notifications' do - expect { subject.call(status) }.to change { - Notification.where(activity_type: 'Favourite', from_account: jeff, account: alice).count - }.from(1).to(0) - end - - def delete_delivery(target, status) - a_request(:post, target.inbox_url) - .with(body: delete_activity_for(status)) - end - - def delete_activity_for(status) - hash_including( - 'type' => 'Delete', - 'object' => { - 'type' => 'Tombstone', - 'id' => ActivityPub::TagManager.instance.uri_for(status), - 'atomUri' => OStatus::TagManager.instance.uri_for(status), - } - ) - end - end - - context 'when removed status is a private self-reblog' do - let!(:original_status) { Fabricate(:status, account: alice, text: 'Hello ThisIsASecret', visibility: :private) } - let!(:status) { ReblogService.new.call(alice, original_status) } - - it 'sends Undo activity to followers' do - subject.call(status) - - expect(undo_delivery(hank, original_status)) - .to have_been_made.once - end - end - - context 'when removed status is public self-reblog' do - let!(:original_status) { Fabricate(:status, account: alice, text: 'Hello ThisIsASecret', visibility: :public) } - let!(:status) { ReblogService.new.call(alice, original_status) } - - it 'sends Undo activity to followers' do - subject.call(status) - - expect(undo_delivery(hank, original_status)) - .to have_been_made.once - end - end - - context 'when removed status is a reblog of a non-follower' do - let!(:original_status) { Fabricate(:status, account: bill, text: 'Hello ThisIsASecret', visibility: :public) } - let!(:status) { ReblogService.new.call(alice, original_status) } - - it 'sends Undo activity to followers' do - subject.call(status) - - expect(undo_delivery(bill, original_status)) - .to have_been_made.once - end - end - - def undo_delivery(target, status) - a_request(:post, target.inbox_url) - .with(body: undo_activity_for(status)) - end - - def undo_activity_for(status) - hash_including( - 'type' => 'Undo', - 'object' => hash_including( - 'type' => 'Announce', - 'object' => ActivityPub::TagManager.instance.uri_for(status) - ) - ) - end -end diff --git a/spec/services/report_service_spec.rb b/spec/services/report_service_spec.rb deleted file mode 100644 index 6518c5c27a..0000000000 --- a/spec/services/report_service_spec.rb +++ /dev/null @@ -1,185 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ReportService do - subject { described_class.new } - - let(:source_account) { Fabricate(:account) } - let(:target_account) { Fabricate(:account) } - - context 'with a local account' do - it 'has a uri' do - report = subject.call(source_account, target_account) - expect(report.uri).to_not be_nil - end - end - - context 'with a remote account' do - let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } - let(:forward) { false } - - before do - stub_request(:post, 'http://example.com/inbox').to_return(status: 200) - end - - it 'does not have an application' do - report = subject.call(source_account, remote_account) - expect(report.application).to be_nil - end - - context 'when forward is true', :inline_jobs do - let(:forward) { true } - - it 'sends ActivityPub payload when forward is true' do - subject.call(source_account, remote_account, forward: forward) - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made - end - - it 'has an uri' do - report = subject.call(source_account, remote_account, forward: forward) - expect(report.uri).to_not be_nil - end - - context 'when reporting a reply on a different remote server' do - let(:remote_thread_account) { Fabricate(:account, domain: 'foo.com', protocol: :activitypub, inbox_url: 'http://foo.com/inbox') } - let(:reported_status) { Fabricate(:status, account: remote_account, thread: Fabricate(:status, account: remote_thread_account)) } - - before do - stub_request(:post, 'http://foo.com/inbox').to_return(status: 200) - end - - context 'when forward_to_domains includes both the replied-to domain and the origin domain' do - it 'sends ActivityPub payload to both the author of the replied-to post and the reported user' do - subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward, forward_to_domains: [remote_account.domain, remote_thread_account.domain]) - expect(a_request(:post, 'http://foo.com/inbox')).to have_been_made - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made - end - end - - context 'when forward_to_domains includes only the replied-to domain' do - it 'sends ActivityPub payload only to the author of the replied-to post' do - subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward, forward_to_domains: [remote_thread_account.domain]) - expect(a_request(:post, 'http://foo.com/inbox')).to have_been_made - expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made - end - end - - context 'when forward_to_domains does not include the replied-to domain' do - it 'does not send ActivityPub payload to the author of the replied-to post' do - subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward) - expect(a_request(:post, 'http://foo.com/inbox')).to_not have_been_made - end - end - end - - context 'when reporting a reply on the same remote server as the person being replied-to' do - let(:remote_thread_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } - let(:reported_status) { Fabricate(:status, account: remote_account, thread: Fabricate(:status, account: remote_thread_account)) } - - context 'when forward_to_domains includes both the replied-to domain and the origin domain' do - it 'sends ActivityPub payload only once' do - subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward, forward_to_domains: [remote_account.domain]) - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once - end - end - - context 'when forward_to_domains does not include the replied-to domain' do - it 'sends ActivityPub payload only once' do - subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward) - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once - end - end - end - end - - context 'when forward is false' do - it 'does not send anything' do - subject.call(source_account, remote_account, forward: forward) - expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made - end - end - end - - context 'when passed an application' do - let(:application) { Fabricate(:application) } - - it 'has an application' do - report = subject.call(source_account, target_account, application: application) - expect(report.application).to eq application - end - end - - context 'when the reported status is a DM' do - subject do - -> { described_class.new.call(source_account, target_account, status_ids: [status.id]) } - end - - let(:status) { Fabricate(:status, account: target_account, visibility: :direct) } - - context 'when it is addressed to the reporter' do - before do - status.mentions.create(account: source_account) - end - - it 'creates a report' do - expect { subject.call }.to change { target_account.targeted_reports.count }.from(0).to(1) - end - - it 'attaches the DM to the report' do - subject.call - expect(target_account.targeted_reports.pluck(:status_ids)).to eq [[status.id]] - end - end - - context 'when it is not addressed to the reporter' do - it 'errors out' do - expect { subject.call }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'when the reporter is remote' do - let(:source_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/1') } - - context 'when it is addressed to the reporter' do - before do - status.mentions.create(account: source_account) - end - - it 'creates a report' do - expect { subject.call }.to change { target_account.targeted_reports.count }.from(0).to(1) - end - - it 'attaches the DM to the report' do - subject.call - expect(target_account.targeted_reports.pluck(:status_ids)).to eq [[status.id]] - end - end - - context 'when it is not addressed to the reporter' do - it 'does not add the DM to the report' do - subject.call - expect(target_account.targeted_reports.pluck(:status_ids)).to eq [[]] - end - end - end - end - - context 'when other reports already exist for the same target' do - subject do - -> { described_class.new.call(source_account, target_account) } - end - - before do - Fabricate(:report, target_account: target_account) - source_account.user.settings['notification_emails.report'] = true - source_account.user.save - end - - it 'does not send an e-mail' do - emails = capture_emails { subject.call } - - expect(emails).to be_empty - end - end -end diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb deleted file mode 100644 index e0084a1579..0000000000 --- a/spec/services/resolve_account_service_spec.rb +++ /dev/null @@ -1,238 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ResolveAccountService do - subject { described_class.new } - - before do - stub_request(:get, 'https://example.com/.well-known/host-meta').to_return(status: 404) - stub_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png').to_return(request_fixture('avatar.txt')) - stub_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com').to_return(request_fixture('activitypub-webfinger.txt')) - stub_request(:get, 'https://ap.example.com/users/foo').to_return(request_fixture('activitypub-actor.txt')) - stub_request(:get, 'https://ap.example.com/users/foo.atom').to_return(request_fixture('activitypub-feed.txt')) - stub_request(:get, %r{https://ap\.example\.com/users/foo/\w+}).to_return(status: 404) - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:hoge@example.com').to_return(status: 410) - end - - context 'when using skip_webfinger' do - context 'when account is known' do - let!(:remote_account) { Fabricate(:account, username: 'foo', domain: 'ap.example.com', protocol: 'activitypub') } - - context 'when domain is banned' do - before { Fabricate(:domain_block, domain: 'ap.example.com', severity: :suspend) } - - it 'does not return an account' do - expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to be_nil - end - - it 'does not make a webfinger query' do - subject.call('foo@ap.example.com', skip_webfinger: true) - expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made - end - end - - context 'when domain is not banned' do - it 'returns the expected account' do - expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to eq remote_account - end - - it 'does not make a webfinger query' do - subject.call('foo@ap.example.com', skip_webfinger: true) - expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made - end - end - end - - context 'when account is not known' do - it 'does not return an account' do - expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to be_nil - end - - it 'does not make a webfinger query' do - subject.call('foo@ap.example.com', skip_webfinger: true) - expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made - end - end - end - - context 'when there is an LRDD endpoint but no resolvable account' do - before do - stub_request(:get, 'https://quitter.no/.well-known/host-meta').to_return(request_fixture('.host-meta.txt')) - stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no').to_return(status: 404) - end - - it 'returns nil' do - expect(subject.call('catsrgr8@quitter.no')).to be_nil - end - end - - context 'when there is no LRDD endpoint nor resolvable account' do - before do - stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:catsrgr8@example.com').to_return(status: 404) - end - - it 'returns nil' do - expect(subject.call('catsrgr8@example.com')).to be_nil - end - end - - context 'when webfinger returns http gone' do - context 'with a previously known account' do - before do - Fabricate(:account, username: 'hoge', domain: 'example.com', last_webfingered_at: nil) - allow(AccountDeletionWorker).to receive(:perform_async) - end - - it 'returns nil' do - expect(subject.call('hoge@example.com')).to be_nil - end - - it 'queues account deletion worker' do - subject.call('hoge@example.com') - expect(AccountDeletionWorker).to have_received(:perform_async) - end - end - - context 'with a previously unknown account' do - it 'returns nil' do - expect(subject.call('hoge@example.com')).to be_nil - end - end - end - - context 'with a legitimate webfinger redirection' do - before do - webfinger = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'returns new remote account' do - account = subject.call('Foo@redirected.example.com') - - expect(account.activitypub?).to be true - expect(account.acct).to eq 'foo@ap.example.com' - expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' - end - end - - context 'with a misconfigured redirection' do - before do - webfinger = { subject: 'acct:Foo@redirected.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'returns new remote account' do - account = subject.call('Foo@redirected.example.com') - - expect(account.activitypub?).to be true - expect(account.acct).to eq 'foo@ap.example.com' - expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' - end - end - - context 'with too many webfinger redirections' do - before do - webfinger = { subject: 'acct:foo@evil.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - webfinger2 = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] } - stub_request(:get, 'https://evil.example.com/.well-known/webfinger?resource=acct:foo@evil.example.com').to_return(body: Oj.dump(webfinger2), headers: { 'Content-Type': 'application/jrd+json' }) - end - - it 'does not return a new remote account' do - expect(subject.call('Foo@redirected.example.com')).to be_nil - end - end - - context 'with webfinger response subject missing a host value' do - let(:body) { Oj.dump({ subject: 'user@' }) } - let(:url) { 'https://host.example/.well-known/webfinger?resource=acct:user@host.example' } - - before do - stub_request(:get, url).to_return(status: 200, body: body) - end - - it 'returns nil with incomplete subject in response' do - expect(subject.call('user@host.example')).to be_nil - end - end - - context 'with an ActivityPub account' do - it 'returns new remote account' do - account = subject.call('foo@ap.example.com') - - expect(account.activitypub?).to be true - expect(account.domain).to eq 'ap.example.com' - expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' - end - - context 'with multiple types' do - before do - stub_request(:get, 'https://ap.example.com/users/foo').to_return(request_fixture('activitypub-actor-individual.txt')) - end - - it 'returns new remote account' do - account = subject.call('foo@ap.example.com') - - expect(account.activitypub?).to be true - expect(account.domain).to eq 'ap.example.com' - expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' - expect(account.actor_type).to eq 'Person' - end - end - end - - context 'with an already-known actor changing acct: URI' do - let!(:duplicate) { Fabricate(:account, username: 'foo', domain: 'old.example.com', uri: 'https://ap.example.com/users/foo') } - let!(:status) { Fabricate(:status, account: duplicate, text: 'foo') } - - it 'returns new remote account' do - account = subject.call('foo@ap.example.com') - - expect(account.activitypub?).to be true - expect(account.domain).to eq 'ap.example.com' - expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' - expect(account.uri).to eq 'https://ap.example.com/users/foo' - end - - it 'merges accounts', :inline_jobs do - account = subject.call('foo@ap.example.com') - - expect(status.reload.account_id).to eq account.id - expect(Account.where(uri: account.uri).count).to eq 1 - end - end - - context 'with an already-known acct: URI changing ActivityPub id' do - let!(:old_account) { Fabricate(:account, username: 'foo', domain: 'ap.example.com', uri: 'https://old.example.com/users/foo', last_webfingered_at: nil) } - let!(:status) { Fabricate(:status, account: old_account, text: 'foo') } - - it 'returns new remote account' do - account = subject.call('foo@ap.example.com') - - expect(account.activitypub?).to be true - expect(account.domain).to eq 'ap.example.com' - expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' - expect(account.uri).to eq 'https://ap.example.com/users/foo' - expect(status.reload.account).to eq(account) - end - end - - it 'processes one remote account at a time using locks' do - fail_occurred = false - return_values = Concurrent::Array.new - - multi_threaded_execution(5) do - begin - return_values << described_class.new.call('foo@ap.example.com') - rescue ActiveRecord::RecordNotUnique - fail_occurred = true - ensure - RedisConfiguration.pool.checkin if Thread.current[:redis] - end - end - - expect(fail_occurred).to be false - expect(return_values).to_not include(nil) - end -end diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb deleted file mode 100644 index 3d59a55f10..0000000000 --- a/spec/services/resolve_url_service_spec.rb +++ /dev/null @@ -1,180 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe ResolveURLService do - subject { described_class.new } - - describe '#call' do - it 'returns nil when there is no resource url' do - url = 'http://example.com/missing-resource' - Fabricate(:account, uri: url, domain: 'example.com') - service = instance_double(FetchResourceService) - - allow(FetchResourceService).to receive(:new).and_return service - allow(service).to receive(:response_code).and_return(404) - allow(service).to receive(:call).with(url).and_return(nil) - - expect(subject.call(url)).to be_nil - end - - it 'returns known account on temporary error' do - url = 'http://example.com/missing-resource' - known_account = Fabricate(:account, uri: url, domain: 'example.com') - service = instance_double(FetchResourceService) - - allow(FetchResourceService).to receive(:new).and_return service - allow(service).to receive(:response_code).and_return(500) - allow(service).to receive(:call).with(url).and_return(nil) - - expect(subject.call(url)).to eq known_account - end - - context 'when searching for a remote private status' do - let(:account) { Fabricate(:account) } - let(:poster) { Fabricate(:account, domain: 'example.com') } - let(:url) { 'https://example.com/@foo/42' } - let(:uri) { 'https://example.com/users/foo/statuses/42' } - let!(:status) { Fabricate(:status, url: url, uri: uri, account: poster, visibility: :private) } - - before do - stub_request(:get, url).to_return(status: 404) if url.present? - stub_request(:get, uri).to_return(status: 404) - end - - context 'when the account follows the poster' do - before do - account.follow!(poster) - end - - context 'when the status uses Mastodon-style URLs' do - let(:url) { 'https://example.com/@foo/42' } - let(:uri) { 'https://example.com/users/foo/statuses/42' } - - it 'returns status by url' do - expect(subject.call(url, on_behalf_of: account)).to eq(status) - end - - it 'returns status by uri' do - expect(subject.call(uri, on_behalf_of: account)).to eq(status) - end - end - - context 'when the status uses pleroma-style URLs' do - let(:url) { nil } - let(:uri) { 'https://example.com/objects/0123-456-789-abc-def' } - - it 'returns status by uri' do - expect(subject.call(uri, on_behalf_of: account)).to eq(status) - end - end - end - - context 'when the account does not follow the poster' do - context 'when the status uses Mastodon-style URLs' do - let(:url) { 'https://example.com/@foo/42' } - let(:uri) { 'https://example.com/users/foo/statuses/42' } - - it 'does not return the status by url' do - expect(subject.call(url, on_behalf_of: account)).to be_nil - end - - it 'does not return the status by uri' do - expect(subject.call(uri, on_behalf_of: account)).to be_nil - end - end - - context 'when the status uses pleroma-style URLs' do - let(:url) { nil } - let(:uri) { 'https://example.com/objects/0123-456-789-abc-def' } - - it 'returns status by uri' do - expect(subject.call(uri, on_behalf_of: account)).to be_nil - end - end - end - end - - context 'when searching for a local private status' do - let(:account) { Fabricate(:account) } - let(:poster) { Fabricate(:account) } - let!(:status) { Fabricate(:status, account: poster, visibility: :private) } - let(:url) { ActivityPub::TagManager.instance.url_for(status) } - let(:uri) { ActivityPub::TagManager.instance.uri_for(status) } - - context 'when the account follows the poster' do - before do - account.follow!(poster) - end - - it 'returns status by url' do - expect(subject.call(url, on_behalf_of: account)).to eq(status) - end - - it 'returns status by uri' do - expect(subject.call(uri, on_behalf_of: account)).to eq(status) - end - end - - context 'when the account does not follow the poster' do - it 'does not return the status by url' do - expect(subject.call(url, on_behalf_of: account)).to be_nil - end - - it 'does not return the status by uri' do - expect(subject.call(uri, on_behalf_of: account)).to be_nil - end - end - end - - context 'when searching for a link that redirects to a local public status' do - let(:account) { Fabricate(:account) } - let(:poster) { Fabricate(:account) } - let!(:status) { Fabricate(:status, account: poster, visibility: :public) } - let(:url) { 'https://link.to/foobar' } - let(:status_url) { ActivityPub::TagManager.instance.url_for(status) } - let(:uri) { ActivityPub::TagManager.instance.uri_for(status) } - - before do - stub_request(:get, url).to_return(status: 302, headers: { 'Location' => status_url }) - body = ActiveModelSerializers::SerializableResource.new(status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter).to_json - stub_request(:get, status_url).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' }) - stub_request(:get, uri).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' }) - end - - it 'returns status by url' do - expect(subject.call(url, on_behalf_of: account)).to eq(status) - end - end - - context 'when searching for a local link of a remote private status' do - let(:account) { Fabricate(:account) } - let(:poster) { Fabricate(:account, username: 'foo', domain: 'example.com') } - let(:url) { 'https://example.com/@foo/42' } - let(:uri) { 'https://example.com/users/foo/statuses/42' } - let!(:status) { Fabricate(:status, url: url, uri: uri, account: poster, visibility: :private) } - let(:search_url) { "https://#{Rails.configuration.x.local_domain}/@foo@example.com/#{status.id}" } - - before do - stub_request(:get, url).to_return(status: 404) if url.present? - stub_request(:get, uri).to_return(status: 404) - end - - context 'when the account follows the poster' do - before do - account.follow!(poster) - end - - it 'returns the status' do - expect(subject.call(search_url, on_behalf_of: account)).to eq(status) - end - end - - context 'when the account does not follow the poster' do - it 'does not return the status' do - expect(subject.call(search_url, on_behalf_of: account)).to be_nil - end - end - end - end -end diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb deleted file mode 100644 index 394ee7c3a6..0000000000 --- a/spec/services/search_service_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe SearchService do - subject { described_class.new } - - describe '#call' do - describe 'with a blank query' do - it 'returns empty results without searching' do - allow(AccountSearchService).to receive(:new) - allow(Tag).to receive(:search_for) - results = subject.call('', nil, 10) - - expect(results).to eq(empty_results) - expect(AccountSearchService).to_not have_received(:new) - expect(Tag).to_not have_received(:search_for) - end - end - - describe 'with an url query' do - let(:query) { 'http://test.host/query' } - - context 'when it does not find anything' do - it 'returns the empty results' do - service = instance_double(ResolveURLService, call: nil) - allow(ResolveURLService).to receive(:new).and_return(service) - results = subject.call(query, nil, 10, resolve: true) - - expect(service).to have_received(:call).with(query, on_behalf_of: nil) - expect(results).to eq empty_results - end - end - - context 'when it finds an account' do - it 'includes the account in the results' do - account = Account.new - service = instance_double(ResolveURLService, call: account) - allow(ResolveURLService).to receive(:new).and_return(service) - - results = subject.call(query, nil, 10, resolve: true) - expect(service).to have_received(:call).with(query, on_behalf_of: nil) - expect(results).to eq empty_results.merge(accounts: [account]) - end - end - - context 'when it finds a status' do - it 'includes the status in the results' do - status = Status.new - service = instance_double(ResolveURLService, call: status) - allow(ResolveURLService).to receive(:new).and_return(service) - - results = subject.call(query, nil, 10, resolve: true) - expect(service).to have_received(:call).with(query, on_behalf_of: nil) - expect(results).to eq empty_results.merge(statuses: [status]) - end - end - end - - describe 'with a non-url query' do - context 'when it matches an account' do - it 'includes the account in the results' do - query = 'username' - account = Account.new - service = instance_double(AccountSearchService, call: [account]) - allow(AccountSearchService).to receive(:new).and_return(service) - - results = subject.call(query, nil, 10) - expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false, start_with_hashtag: false, use_searchable_text: true, following: false) - expect(results).to eq empty_results.merge(accounts: [account]) - end - end - - context 'when it matches a tag' do - it 'includes the tag in the results' do - query = '#tag' - tag = Tag.new - allow(Tag).to receive(:search_for).with('tag', 10, 0, { exclude_unreviewed: nil }).and_return([tag]) - - results = subject.call(query, nil, 10) - expect(Tag).to have_received(:search_for).with('tag', 10, 0, exclude_unreviewed: nil) - expect(results).to eq empty_results.merge(hashtags: [tag]) - end - end - end - end - - def empty_results - { accounts: [], hashtags: [], statuses: [] } - end -end diff --git a/spec/services/software_update_check_service_spec.rb b/spec/services/software_update_check_service_spec.rb deleted file mode 100644 index a1eb9d86e9..0000000000 --- a/spec/services/software_update_check_service_spec.rb +++ /dev/null @@ -1,158 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe SoftwareUpdateCheckService do - subject { described_class.new } - - shared_examples 'when the feature is enabled' do - let(:full_update_check_url) { "#{update_check_url}?version=#{Mastodon::Version.to_s.split('+')[0]}" } - - let(:devops_role) { Fabricate(:user_role, name: 'DevOps', permissions: UserRole::FLAGS[:view_devops]) } - let(:owner_user) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')) } - let(:old_devops_user) { Fabricate(:user) } - let(:none_user) { Fabricate(:user, role: devops_role) } - let(:patch_user) { Fabricate(:user, role: devops_role) } - let(:critical_user) { Fabricate(:user, role: devops_role) } - - around do |example| - queue_adapter = ActiveJob::Base.queue_adapter - ActiveJob::Base.queue_adapter = :test - - example.run - - ActiveJob::Base.queue_adapter = queue_adapter - end - - before do - Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: false) - Fabricate(:software_update, version: '42.13.12', type: 'major', urgent: false) - - owner_user.settings.update('notification_emails.software_updates': 'all') - owner_user.save! - - old_devops_user.settings.update('notification_emails.software_updates': 'all') - old_devops_user.save! - - none_user.settings.update('notification_emails.software_updates': 'none') - none_user.save! - - patch_user.settings.update('notification_emails.software_updates': 'patch') - patch_user.save! - - critical_user.settings.update('notification_emails.software_updates': 'critical') - critical_user.save! - end - - context 'when the update server errors out' do - before do - stub_request(:get, full_update_check_url).to_return(status: 404) - end - - it 'deletes outdated update records but keeps valid update records' do - expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12']).to(['42.13.12']) - end - end - - context 'when the server returns new versions' do - let(:server_json) do - { - updatesAvailable: [ - { - version: '4.2.1', - urgent: false, - type: 'patch', - releaseNotes: 'https://github.com/mastodon/mastodon/releases/v4.2.1', - }, - { - version: '4.3.0', - urgent: false, - type: 'minor', - releaseNotes: 'https://github.com/mastodon/mastodon/releases/v4.3.0', - }, - { - version: '5.0.0', - urgent: false, - type: 'minor', - releaseNotes: 'https://github.com/mastodon/mastodon/releases/v5.0.0', - }, - ], - } - end - - before do - stub_request(:get, full_update_check_url).to_return(body: Oj.dump(server_json)) - end - - it 'updates the list of known updates' do - expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12']).to(['4.2.1', '4.3.0', '5.0.0']) - end - - context 'when no update is urgent' do - it 'sends e-mail notifications according to settings', :aggregate_failures do - expect { subject.call }.to have_enqueued_mail(AdminMailer, :new_software_updates) - .with(hash_including(params: { recipient: owner_user.account })).once - .and(have_enqueued_mail(AdminMailer, :new_software_updates).with(hash_including(params: { recipient: patch_user.account })).once) - .and(have_enqueued_mail.at_most(2)) - end - end - - context 'when an update is urgent' do - let(:server_json) do - { - updatesAvailable: [ - { - version: '5.0.0', - urgent: true, - type: 'minor', - releaseNotes: 'https://github.com/mastodon/mastodon/releases/v5.0.0', - }, - ], - } - end - - it 'sends e-mail notifications according to settings', :aggregate_failures do - expect { subject.call }.to have_enqueued_mail(AdminMailer, :new_critical_software_updates) - .with(hash_including(params: { recipient: owner_user.account })).once - .and(have_enqueued_mail(AdminMailer, :new_critical_software_updates).with(hash_including(params: { recipient: patch_user.account })).once) - .and(have_enqueued_mail(AdminMailer, :new_critical_software_updates).with(hash_including(params: { recipient: critical_user.account })).once) - .and(have_enqueued_mail.at_most(3)) - end - end - end - end - - context 'when update checking is disabled' do - around do |example| - ClimateControl.modify UPDATE_CHECK_URL: '' do - example.run - end - end - - before do - Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: false) - end - - it 'deletes outdated update records' do - expect { subject.call }.to change(SoftwareUpdate, :count).from(1).to(0) - end - end - - context 'when using the default update checking API' do - let(:update_check_url) { 'https://api.joinmastodon.org/update-check' } - - it_behaves_like 'when the feature is enabled' - end - - context 'when using a custom update check URL' do - let(:update_check_url) { 'https://api.example.com/update_check' } - - around do |example| - ClimateControl.modify UPDATE_CHECK_URL: 'https://api.example.com/update_check' do - example.run - end - end - - it_behaves_like 'when the feature is enabled' - end -end diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb deleted file mode 100644 index 4a2f494e0c..0000000000 --- a/spec/services/suspend_account_service_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe SuspendAccountService, :inline_jobs do - shared_examples 'common behavior' do - subject { described_class.new.call(account) } - - let!(:local_follower) { Fabricate(:user, current_sign_in_at: 1.hour.ago).account } - let!(:list) { Fabricate(:list, account: local_follower) } - - before do - allow(FeedManager.instance).to receive_messages(unmerge_from_home: nil, unmerge_from_list: nil) - - local_follower.follow!(account) - list.accounts << account - - account.suspend! - - Fabricate(:media_attachment, file: attachment_fixture('boop.ogg'), account: account) - end - - it 'unmerges from feeds of local followers and changes file mode and preserves suspended flag' do - expect { subject } - .to change_file_mode - .and not_change_suspended_flag - expect(FeedManager.instance).to have_received(:unmerge_from_home).with(account, local_follower) - expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list) - end - - def change_file_mode - change { File.stat(account.media_attachments.first.file.path).mode } - end - - def not_change_suspended_flag - not_change(account, :suspended?) - end - end - - describe 'suspending a local account' do - def match_update_actor_request(req, account) - json = JSON.parse(req.body) - actor_id = ActivityPub::TagManager.instance.uri_for(account) - json['type'] == 'Update' && json['actor'] == actor_id && json['object']['id'] == actor_id && json['object']['suspended'] - end - - before do - stub_request(:post, 'https://alice.com/inbox').to_return(status: 201) - stub_request(:post, 'https://bob.com/inbox').to_return(status: 201) - end - - include_examples 'common behavior' do - let!(:account) { Fabricate(:account) } - let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') } - let!(:remote_reporter) { Fabricate(:account, uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub, domain: 'bob.com') } - - before do - Fabricate(:report, account: remote_reporter, target_account: account) - remote_follower.follow!(account) - end - - it 'sends an Update actor activity to followers and reporters' do - subject - expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once - expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once - end - end - end - - describe 'suspending a remote account' do - def match_reject_follow_request(req, account, followee) - json = JSON.parse(req.body) - json['type'] == 'Reject' && json['actor'] == ActivityPub::TagManager.instance.uri_for(followee) && json['object']['actor'] == account.uri - end - - before do - stub_request(:post, 'https://bob.com/inbox').to_return(status: 201) - end - - include_examples 'common behavior' do - let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } - let!(:local_followee) { Fabricate(:account) } - - before do - account.follow!(local_followee) - end - - it 'sends a Reject Follow activity', :aggregate_failures do - subject - - expect(a_request(:post, account.inbox_url).with { |req| match_reject_follow_request(req, account, local_followee) }).to have_been_made.once - end - end - end -end diff --git a/spec/services/tag_search_service_spec.rb b/spec/services/tag_search_service_spec.rb deleted file mode 100644 index de42e54071..0000000000 --- a/spec/services/tag_search_service_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe TagSearchService do - describe '#call' do - let!(:one) { Fabricate(:tag, name: 'one') } - - before { Fabricate(:tag, name: 'two') } - - it 'runs a search for tags' do - results = subject.call('#one', limit: 5) - - expect(results) - .to have_attributes( - size: 1, - first: eq(one) - ) - end - end -end diff --git a/spec/services/translate_status_service_spec.rb b/spec/services/translate_status_service_spec.rb deleted file mode 100644 index 0779fbbe6c..0000000000 --- a/spec/services/translate_status_service_spec.rb +++ /dev/null @@ -1,234 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe TranslateStatusService do - subject(:service) { described_class.new } - - let(:status) { Fabricate(:status, text: text, spoiler_text: spoiler_text, language: 'en', preloadable_poll: poll, media_attachments: media_attachments) } - let(:text) { 'Hello' } - let(:spoiler_text) { '' } - let(:poll) { nil } - let(:media_attachments) { [] } - - before do - Fabricate(:custom_emoji, shortcode: 'highfive') - end - - describe '#call' do - before do - translation_service = TranslationService.new - allow(translation_service).to receive(:languages).and_return({ 'en' => ['es'] }) - allow(translation_service).to receive(:translate) do |texts| - texts.map do |text| - TranslationService::Translation.new( - text: text.gsub('Hello', 'Hola').gsub('higfive', 'cincoaltos'), - detected_source_language: 'en', - provider: 'Dummy' - ) - end - end - - allow(TranslationService).to receive_messages(configured?: true, configured: translation_service) - end - - it 'returns translated status content' do - expect(service.call(status, 'es').content).to eq '

Hola

' - end - - it 'returns source language' do - expect(service.call(status, 'es').detected_source_language).to eq 'en' - end - - it 'returns translation provider' do - expect(service.call(status, 'es').provider).to eq 'Dummy' - end - - it 'returns original status' do - expect(service.call(status, 'es').status).to eq status - end - - describe 'status has content with custom emoji' do - let(:text) { 'Hello & :highfive:' } - - it 'does not translate shortcode' do - expect(service.call(status, 'es').content).to eq '

Hola & :highfive:

' - end - end - - describe 'status has no spoiler_text' do - it 'returns an empty string' do - expect(service.call(status, 'es').spoiler_text).to eq '' - end - end - - describe 'status has spoiler_text' do - let(:spoiler_text) { 'Hello & Hello!' } - - it 'translates the spoiler text' do - expect(service.call(status, 'es').spoiler_text).to eq 'Hola & Hola!' - end - end - - describe 'status has spoiler_text with custom emoji' do - let(:spoiler_text) { 'Hello :highfive:' } - - it 'does not translate shortcode' do - expect(service.call(status, 'es').spoiler_text).to eq 'Hola :highfive:' - end - end - - describe 'status has spoiler_text with unmatched custom emoji' do - let(:spoiler_text) { 'Hello :Hello:' } - - it 'translates the invalid shortcode' do - expect(service.call(status, 'es').spoiler_text).to eq 'Hola :Hola:' - end - end - - describe 'status has poll' do - let(:poll) { Fabricate(:poll, options: ['Hello 1', 'Hello 2']) } - - it 'translates the poll option title' do - status_translation = service.call(status, 'es') - expect(status_translation.poll_options.size).to eq 2 - expect(status_translation.poll_options.first.title).to eq 'Hola 1' - end - end - - describe 'status has media attachment' do - let(:media_attachments) { [Fabricate(:media_attachment, description: 'Hello & :highfive:')] } - - it 'translates the media attachment description' do - status_translation = service.call(status, 'es') - - media_attachment = status_translation.media_attachments.first - expect(media_attachment.id).to eq media_attachments.first.id - expect(media_attachment.description).to eq 'Hola & :highfive:' - end - end - end - - describe '#source_texts' do - before do - service.instance_variable_set(:@status, status) - end - - describe 'status only has content' do - it 'returns formatted content' do - expect(service.send(:source_texts)).to eq({ content: '

Hello

' }) - end - end - - describe 'status content contains custom emoji' do - let(:status) { Fabricate(:status, text: 'Hello :highfive:') } - - it 'returns formatted content' do - source_texts = service.send(:source_texts) - expect(source_texts[:content]).to eq '

Hello :highfive:

' - end - end - - describe 'status content contains tags' do - let(:status) { Fabricate(:status, text: 'Hello #hola') } - - it 'returns formatted content' do - source_texts = service.send(:source_texts) - expect(source_texts[:content]).to include '

Hello :highfive:' - end - end - - describe 'status has poll' do - let(:poll) { Fabricate(:poll, options: %w(Blue Green)) } - - context 'with source texts from the service' do - let!(:source_texts) { service.send(:source_texts) } - - it 'returns formatted poll options' do - expect(source_texts.size).to eq 3 - expect(source_texts.values).to eq %w(

Hello

Blue Green) - end - - it 'has a first key with content' do - expect(source_texts.keys.first).to eq :content - end - - it 'has the first option in the second key with correct options' do - option1 = source_texts.keys.second - expect(option1).to be_a Poll::Option - expect(option1.id).to eq '0' - expect(option1.title).to eq 'Blue' - end - - it 'has the second option in the third key with correct options' do - option2 = source_texts.keys.third - expect(option2).to be_a Poll::Option - expect(option2.id).to eq '1' - expect(option2.title).to eq 'Green' - end - end - end - - describe 'status has poll with custom emoji' do - let(:poll) { Fabricate(:poll, options: ['Blue', 'Green :highfive:']) } - - it 'returns formatted poll options' do - html = service.send(:source_texts).values.last - expect(html).to eq 'Green :highfive:' - end - end - - describe 'status has media attachments' do - let(:text) { '' } - let(:media_attachments) { [Fabricate(:media_attachment, description: 'Hello :highfive:')] } - - it 'returns media attachments without custom emoji rendering' do - source_texts = service.send(:source_texts) - expect(source_texts.size).to eq 1 - - key, text = source_texts.first - expect(key).to eq media_attachments.first - expect(text).to eq 'Hello :highfive:' - end - end - end - - describe '#wrap_emoji_shortcodes' do - before do - service.instance_variable_set(:@status, status) - end - - describe 'string contains custom emoji' do - let(:text) { ':highfive:' } - - it 'renders the emoji' do - html = service.send(:wrap_emoji_shortcodes, 'Hello :highfive:'.html_safe) - expect(html).to eq 'Hello :highfive:' - end - end - end - - describe '#unwrap_emoji_shortcodes' do - describe 'string contains custom emoji' do - it 'inserts the shortcode' do - fragment = service.send(:unwrap_emoji_shortcodes, '

Hello :highfive:!

') - expect(fragment.to_html).to eq '

Hello :highfive:!

' - end - - it 'preserves other attributes than translate=no' do - fragment = service.send(:unwrap_emoji_shortcodes, '

Hello :highfive:!

') - expect(fragment.to_html).to eq '

Hello :highfive:!

' - end - end - end -end diff --git a/spec/services/unallow_domain_service_spec.rb b/spec/services/unallow_domain_service_spec.rb deleted file mode 100644 index 4bf6c54043..0000000000 --- a/spec/services/unallow_domain_service_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UnallowDomainService do - subject { described_class.new } - - let(:bad_domain) { 'evil.org' } - let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: bad_domain) } - let!(:bad_status_harassment) { Fabricate(:status, account: bad_account, text: 'You suck') } - let!(:bad_status_mean) { Fabricate(:status, account: bad_account, text: 'Hahaha') } - let!(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status_mean, file: attachment_fixture('attachment.jpg')) } - let!(:already_banned_account) { Fabricate(:account, username: 'badguy', domain: bad_domain, suspended: true, silenced: true) } - let!(:domain_allow) { Fabricate(:domain_allow, domain: bad_domain) } - - context 'with limited federation mode', :inline_jobs do - before do - allow(Rails.configuration.x).to receive(:limited_federation_mode).and_return(true) - end - - describe '#call' do - it 'makes the domain not allowed and removes accounts from that domain' do - expect { subject.call(domain_allow) } - .to change { bad_domain_allowed }.from(true).to(false) - .and change { bad_domain_account_exists }.from(true).to(false) - - expect { already_banned_account.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { bad_status_harassment.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { bad_status_mean.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { bad_attachment.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end - - context 'without limited federation mode' do - before do - allow(Rails.configuration.x).to receive(:limited_federation_mode).and_return(false) - end - - describe '#call' do - it 'makes the domain not allowed but preserves accounts from the domain' do - expect { subject.call(domain_allow) } - .to change { bad_domain_allowed }.from(true).to(false) - .and not_change { bad_domain_account_exists }.from(true) - - expect { bad_status_harassment.reload }.to_not raise_error - expect { bad_status_mean.reload }.to_not raise_error - expect { bad_attachment.reload }.to_not raise_error - end - end - end - - def bad_domain_allowed - DomainAllow.allowed?(bad_domain) - end - - def bad_domain_account_exists - Account.exists?(domain: bad_domain) - end -end diff --git a/spec/services/unblock_domain_service_spec.rb b/spec/services/unblock_domain_service_spec.rb deleted file mode 100644 index 289ddfc218..0000000000 --- a/spec/services/unblock_domain_service_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe UnblockDomainService do - subject { described_class.new } - - describe 'call' do - let!(:independently_suspended) { Fabricate(:account, domain: 'example.com', suspended_at: 1.hour.ago) } - let!(:independently_silenced) { Fabricate(:account, domain: 'example.com', silenced_at: 1.hour.ago) } - let!(:domain_block) { Fabricate(:domain_block, domain: 'example.com') } - let!(:silenced) { Fabricate(:account, domain: 'example.com', silenced_at: domain_block.created_at) } - let!(:suspended) { Fabricate(:account, domain: 'example.com', suspended_at: domain_block.created_at) } - - it 'unsilences accounts and removes block' do - domain_block.update(severity: :silence) - - subject.call(domain_block) - expect_deleted_domain_block - expect(silenced.reload.silenced?).to be false - expect(suspended.reload.suspended?).to be true - expect(independently_suspended.reload.suspended?).to be true - expect(independently_silenced.reload.silenced?).to be true - end - - it 'unsuspends accounts and removes block' do - domain_block.update(severity: :suspend) - - subject.call(domain_block) - expect_deleted_domain_block - expect(suspended.reload.suspended?).to be false - expect(silenced.reload.silenced?).to be false - expect(independently_suspended.reload.suspended?).to be true - expect(independently_silenced.reload.silenced?).to be true - end - end - - def expect_deleted_domain_block - expect { domain_block.reload }.to raise_error(ActiveRecord::RecordNotFound) - end -end diff --git a/spec/services/unblock_service_spec.rb b/spec/services/unblock_service_spec.rb deleted file mode 100644 index 6132e74415..0000000000 --- a/spec/services/unblock_service_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UnblockService do - subject { described_class.new } - - let(:sender) { Fabricate(:account, username: 'alice') } - - describe 'local' do - let(:bob) { Fabricate(:account) } - - before do - sender.block!(bob) - subject.call(sender, bob) - end - - it 'destroys the blocking relation' do - expect(sender.blocking?(bob)).to be false - end - end - - describe 'remote ActivityPub' do - let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } - - before do - sender.block!(bob) - stub_request(:post, 'http://example.com/inbox').to_return(status: 200) - subject.call(sender, bob) - end - - it 'destroys the blocking relation' do - expect(sender.blocking?(bob)).to be false - end - - it 'sends an unblock activity', :inline_jobs do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once - end - end -end diff --git a/spec/services/unfavourite_service_spec.rb b/spec/services/unfavourite_service_spec.rb deleted file mode 100644 index a714cc0675..0000000000 --- a/spec/services/unfavourite_service_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UnfavouriteService do - describe '#call' do - context 'with a favourited status' do - let(:status) { Fabricate(:status, account: account) } - let!(:favourite) { Fabricate(:favourite, status: status) } - - context 'when the status account is local' do - let(:account) { Fabricate(:account, domain: nil) } - - it 'destroys the favourite' do - subject.call(favourite.account, status) - - expect { favourite.reload } - .to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'when the status account is a remote activitypub account' do - let(:account) { Fabricate(:account, domain: 'host.example', protocol: :activitypub) } - - it 'destroys the favourite and sends a notification' do - subject.call(favourite.account, status) - - expect { favourite.reload } - .to raise_error(ActiveRecord::RecordNotFound) - expect(ActivityPub::DeliveryWorker) - .to have_enqueued_sidekiq_job(anything, favourite.account.id, status.account.inbox_url) - end - end - end - end -end diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb deleted file mode 100644 index 0c206c4b98..0000000000 --- a/spec/services/unfollow_service_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UnfollowService do - subject { described_class.new } - - let(:sender) { Fabricate(:account, username: 'alice') } - - describe 'local' do - let(:bob) { Fabricate(:account, username: 'bob') } - - before do - sender.follow!(bob) - subject.call(sender, bob) - end - - it 'destroys the following relation' do - expect(sender.following?(bob)).to be false - end - end - - describe 'remote ActivityPub', :inline_jobs do - let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } - - before do - sender.follow!(bob) - stub_request(:post, 'http://example.com/inbox').to_return(status: 200) - subject.call(sender, bob) - end - - it 'destroys the following relation' do - expect(sender.following?(bob)).to be false - end - - it 'sends an unfollow activity' do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once - end - end - - describe 'remote ActivityPub (reverse)', :inline_jobs do - let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } - - before do - bob.follow!(sender) - stub_request(:post, 'http://example.com/inbox').to_return(status: 200) - subject.call(bob, sender) - end - - it 'destroys the following relation' do - expect(bob.following?(sender)).to be false - end - - it 'sends a reject activity' do - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once - end - end -end diff --git a/spec/services/unmute_service_spec.rb b/spec/services/unmute_service_spec.rb deleted file mode 100644 index 92c7a70d65..0000000000 --- a/spec/services/unmute_service_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UnmuteService do - describe '#call' do - let!(:account) { Fabricate(:account) } - let!(:target_account) { Fabricate(:account) } - - context 'when account is muting target account' do - before { Fabricate :mute, account: account, target_account: target_account } - - context 'when account follows target_account' do - before { Fabricate :follow, account: account, target_account: target_account } - - it 'removes the account mute and sets up a merge' do - expect { subject.call(account, target_account) } - .to remove_account_mute - expect(MergeWorker).to have_enqueued_sidekiq_job(target_account.id, account.id) - end - end - - context 'when account does not follow target_account' do - it 'removes the account mute and does not create a merge' do - expect { subject.call(account, target_account) } - .to remove_account_mute - expect(MergeWorker).to_not have_enqueued_sidekiq_job(any_args) - end - end - - def remove_account_mute - change { account.reload.muting?(target_account) } - .from(true) - .to(false) - end - end - - context 'when account is not muting target account' do - it 'does nothing and returns' do - expect { subject.call(account, target_account) } - .to_not(change { account.reload.muting?(target_account) }) - expect(MergeWorker).to_not have_enqueued_sidekiq_job(any_args) - end - end - end -end diff --git a/spec/services/unsuspend_account_service_spec.rb b/spec/services/unsuspend_account_service_spec.rb deleted file mode 100644 index 8d4882c37f..0000000000 --- a/spec/services/unsuspend_account_service_spec.rb +++ /dev/null @@ -1,142 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UnsuspendAccountService do - shared_context 'with common context' do - subject { described_class.new.call(account) } - - let!(:local_follower) { Fabricate(:user, current_sign_in_at: 1.hour.ago).account } - let!(:list) { Fabricate(:list, account: local_follower) } - - before do - allow(FeedManager.instance).to receive_messages(merge_into_home: nil, merge_into_list: nil) - - local_follower.follow!(account) - list.accounts << account - - account.unsuspend! - end - end - - describe 'unsuspending a local account' do - def match_update_actor_request(req, account) - json = JSON.parse(req.body) - actor_id = ActivityPub::TagManager.instance.uri_for(account) - json['type'] == 'Update' && json['actor'] == actor_id && json['object']['id'] == actor_id && !json['object']['suspended'] - end - - before do - stub_request(:post, 'https://alice.com/inbox').to_return(status: 201) - stub_request(:post, 'https://bob.com/inbox').to_return(status: 201) - end - - it 'does not change the “suspended” flag' do - expect { subject }.to_not change(account, :suspended?) - end - - include_examples 'with common context' do - let!(:account) { Fabricate(:account) } - let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') } - let!(:remote_reporter) { Fabricate(:account, uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub, domain: 'bob.com') } - - before do - Fabricate(:report, account: remote_reporter, target_account: account) - remote_follower.follow!(account) - end - - it 'merges back into feeds of local followers and sends update', :inline_jobs do - subject - - expect_feeds_merged - expect_updates_sent - end - - def expect_feeds_merged - expect(FeedManager.instance).to have_received(:merge_into_home).with(account, local_follower) - expect(FeedManager.instance).to have_received(:merge_into_list).with(account, list) - end - - def expect_updates_sent - expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once - expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once - end - end - end - - describe 'unsuspending a remote account' do - include_examples 'with common context' do - let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } - let!(:resolve_account_service) { instance_double(ResolveAccountService) } - - before do - allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service) - end - - context 'when the account is not remotely suspended' do - before do - allow(resolve_account_service).to receive(:call).with(account).and_return(account) - end - - it 're-fetches the account, merges feeds, and preserves suspended' do - expect { subject } - .to_not change_suspended_flag - expect_feeds_merged - expect(resolve_account_service).to have_received(:call).with(account) - end - - def expect_feeds_merged - expect(FeedManager.instance).to have_received(:merge_into_home).with(account, local_follower) - expect(FeedManager.instance).to have_received(:merge_into_list).with(account, list) - end - - def change_suspended_flag - change(account, :suspended?) - end - end - - context 'when the account is remotely suspended' do - before do - allow(resolve_account_service).to receive(:call).with(account) do |account| - account.suspend!(origin: :remote) - account - end - end - - it 're-fetches the account, does not merge feeds, marks suspended' do - expect { subject } - .to change_suspended_to_true - expect(resolve_account_service).to have_received(:call).with(account) - expect_feeds_not_merged - end - - def expect_feeds_not_merged - expect(FeedManager.instance).to_not have_received(:merge_into_home).with(account, local_follower) - expect(FeedManager.instance).to_not have_received(:merge_into_list).with(account, list) - end - - def change_suspended_to_true - change(account, :suspended?).from(false).to(true) - end - end - - context 'when the account is remotely deleted' do - before do - allow(resolve_account_service).to receive(:call).with(account).and_return(nil) - end - - it 're-fetches the account and does not merge feeds' do - subject - - expect(resolve_account_service).to have_received(:call).with(account) - expect_feeds_not_merged - end - - def expect_feeds_not_merged - expect(FeedManager.instance).to_not have_received(:merge_into_home).with(account, local_follower) - expect(FeedManager.instance).to_not have_received(:merge_into_list).with(account, list) - end - end - end - end -end diff --git a/spec/services/update_account_service_spec.rb b/spec/services/update_account_service_spec.rb deleted file mode 100644 index d066db481e..0000000000 --- a/spec/services/update_account_service_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UpdateAccountService do - subject { described_class.new } - - describe 'switching form locked to unlocked accounts', :inline_jobs do - let(:account) { Fabricate(:account, locked: true) } - let(:alice) { Fabricate(:account) } - let(:bob) { Fabricate(:account) } - let(:eve) { Fabricate(:account) } - - before do - bob.touch(:silenced_at) - account.mute!(eve) - - FollowService.new.call(alice, account) - FollowService.new.call(bob, account) - FollowService.new.call(eve, account) - - subject.call(account, { locked: false }) - end - - it 'auto-accepts pending follow requests' do - expect(alice.following?(account)).to be true - expect(alice.requested?(account)).to be false - end - - it 'does not auto-accept pending follow requests from silenced users' do - expect(bob.following?(account)).to be false - expect(bob.requested?(account)).to be true - end - - it 'auto-accepts pending follow requests from muted users so as to not leak mute' do - expect(eve.following?(account)).to be true - expect(eve.requested?(account)).to be false - end - end -end diff --git a/spec/services/update_status_service_spec.rb b/spec/services/update_status_service_spec.rb deleted file mode 100644 index 47be53f4fc..0000000000 --- a/spec/services/update_status_service_spec.rb +++ /dev/null @@ -1,186 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UpdateStatusService do - subject { described_class.new } - - context 'when nothing changes' do - let!(:status) { Fabricate(:status, text: 'Foo', language: 'en') } - - before do - allow(ActivityPub::DistributionWorker).to receive(:perform_async) - subject.call(status, status.account_id, text: 'Foo') - end - - it 'does not create an edit' do - expect(status.reload.edits).to be_empty - end - - it 'does not notify anyone' do - expect(ActivityPub::DistributionWorker).to_not have_received(:perform_async) - end - end - - context 'when text changes' do - let(:status) { Fabricate(:status, text: 'Foo') } - let(:preview_card) { Fabricate(:preview_card) } - - before do - PreviewCardsStatus.create(status: status, preview_card: preview_card) - subject.call(status, status.account_id, text: 'Bar') - end - - it 'updates text' do - expect(status.reload.text).to eq 'Bar' - end - - it 'resets preview card' do - expect(status.reload.preview_card).to be_nil - end - - it 'saves edit history' do - expect(status.edits.ordered.pluck(:text)).to eq %w(Foo Bar) - end - end - - context 'when content warning changes' do - let(:status) { Fabricate(:status, text: 'Foo', spoiler_text: '') } - let(:preview_card) { Fabricate(:preview_card) } - - before do - PreviewCardsStatus.create(status: status, preview_card: preview_card) - subject.call(status, status.account_id, text: 'Foo', spoiler_text: 'Bar') - end - - it 'updates content warning' do - expect(status.reload.spoiler_text).to eq 'Bar' - end - - it 'saves edit history' do - expect(status.edits.ordered.pluck(:text, :spoiler_text)).to eq [['Foo', ''], ['Foo', 'Bar']] - end - end - - context 'when media attachments change' do - let!(:status) { Fabricate(:status, text: 'Foo') } - let!(:detached_media_attachment) { Fabricate(:media_attachment, account: status.account) } - let!(:attached_media_attachment) { Fabricate(:media_attachment, account: status.account) } - - before do - status.media_attachments << detached_media_attachment - subject.call(status, status.account_id, text: 'Foo', media_ids: [attached_media_attachment.id]) - end - - it 'updates media attachments' do - expect(status.ordered_media_attachments).to eq [attached_media_attachment] - end - - it 'does not detach detached media attachments' do - expect(detached_media_attachment.reload.status_id).to eq status.id - end - - it 'attaches attached media attachments' do - expect(attached_media_attachment.reload.status_id).to eq status.id - end - - it 'saves edit history' do - expect(status.edits.ordered.pluck(:ordered_media_attachment_ids)).to eq [[detached_media_attachment.id], [attached_media_attachment.id]] - end - end - - context 'when already-attached media changes' do - let!(:status) { Fabricate(:status, text: 'Foo') } - let!(:media_attachment) { Fabricate(:media_attachment, account: status.account, description: 'Old description') } - - before do - status.media_attachments << media_attachment - subject.call(status, status.account_id, text: 'Foo', media_ids: [media_attachment.id], media_attributes: [{ id: media_attachment.id, description: 'New description' }]) - end - - it 'does not detach media attachment' do - expect(media_attachment.reload.status_id).to eq status.id - end - - it 'updates the media attachment description' do - expect(media_attachment.reload.description).to eq 'New description' - end - - it 'saves edit history' do - expect(status.edits.ordered.map { |edit| edit.ordered_media_attachments.map(&:description) }).to eq [['Old description'], ['New description']] - end - end - - context 'when poll changes' do - let(:account) { Fabricate(:account) } - let!(:status) { Fabricate(:status, text: 'Foo', account: account, poll_attributes: { options: %w(Foo Bar), account: account, multiple: false, hide_totals: false, expires_at: 7.days.from_now }) } - let!(:poll) { status.poll } - let!(:voter) { Fabricate(:account) } - - before do - status.update(poll: poll) - VoteService.new.call(voter, poll, [0]) - subject.call(status, status.account_id, text: 'Foo', poll: { options: %w(Bar Baz Foo), expires_in: 5.days.to_i }) - end - - it 'updates poll' do - poll = status.poll.reload - expect(poll.options).to eq %w(Bar Baz Foo) - end - - it 'resets votes' do - poll = status.poll.reload - expect(poll.votes_count).to eq 0 - expect(poll.votes.count).to eq 0 - expect(poll.cached_tallies).to eq [0, 0, 0] - end - - it 'saves edit history' do - expect(status.edits.ordered.pluck(:poll_options)).to eq [%w(Foo Bar), %w(Bar Baz Foo)] - end - - it 'requeues expiration notification' do - poll = status.poll.reload - expect(PollExpirationNotifyWorker).to have_enqueued_sidekiq_job(poll.id).at(poll.expires_at + 5.minutes) - end - end - - context 'when mentions in text change' do - let!(:account) { Fabricate(:account) } - let!(:alice) { Fabricate(:account, username: 'alice') } - let!(:bob) { Fabricate(:account, username: 'bob') } - let!(:status) { PostStatusService.new.call(account, text: 'Hello @alice') } - - before do - subject.call(status, status.account_id, text: 'Hello @bob') - end - - it 'changes mentions' do - expect(status.active_mentions.pluck(:account_id)).to eq [bob.id] - end - - it 'keeps old mentions as silent mentions' do - expect(status.mentions.pluck(:account_id)).to contain_exactly(alice.id, bob.id) - end - end - - context 'when hashtags in text change' do - let!(:account) { Fabricate(:account) } - let!(:status) { PostStatusService.new.call(account, text: 'Hello #foo') } - - before do - subject.call(status, status.account_id, text: 'Hello #bar') - end - - it 'changes tags' do - expect(status.tags.pluck(:name)).to eq %w(bar) - end - end - - it 'notifies ActivityPub about the update' do - status = Fabricate(:status, text: 'Foo') - allow(ActivityPub::DistributionWorker).to receive(:perform_async) - subject.call(status, status.account_id, text: 'Bar') - expect(ActivityPub::DistributionWorker).to have_received(:perform_async) - end -end diff --git a/spec/services/verify_link_service_spec.rb b/spec/services/verify_link_service_spec.rb deleted file mode 100644 index 0ce8c9a904..0000000000 --- a/spec/services/verify_link_service_spec.rb +++ /dev/null @@ -1,183 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe VerifyLinkService do - subject { described_class.new } - - context 'when given a local account' do - let(:account) { Fabricate(:account, username: 'alice') } - let(:field) { Account::Field.new(account, 'name' => 'Website', 'value' => 'http://example.com') } - - before do - stub_request(:head, 'https://redirect.me/abc').to_return(status: 301, headers: { 'Location' => ActivityPub::TagManager.instance.url_for(account) }) - stub_request(:get, 'http://example.com').to_return(status: 200, body: html) - subject.call(field) - end - - context 'when a link contains an back' do - let(:html) do - <<-HTML - - - Follow me on Mastodon - - HTML - end - - it 'marks the field as verified' do - expect(field.verified?).to be true - end - end - - context 'when a link contains an back' do - let(:html) do - <<-HTML - - - Follow me on Mastodon - - HTML - end - - it 'marks the field as verified' do - expect(field.verified?).to be true - end - end - - context 'when a link contains a back' do - let(:html) do - <<-HTML - - - - - HTML - end - - it 'marks the field as verified' do - expect(field.verified?).to be true - end - end - - context 'when a link goes through a redirect back' do - let(:html) do - <<-HTML - - - - - HTML - end - - it 'marks the field as verified' do - expect(field.verified?).to be true - end - end - - context 'when a document is truncated but the link back is valid' do - let(:html) do - <<-HTML - - - - HTML - end - - it 'marks the field as verified' do - expect(field.verified?).to be true - end - end - - context 'when a link tag might be truncated' do - let(:html) do - <<-HTML_TRUNCATED - - - - - - - - Follow me on Mastodon - - HTML - end - - it 'does not mark the field as verified' do - expect(field.verified?).to be false - end - end - end - - context 'when given a remote account' do - let(:account) { Fabricate(:account, username: 'alice', domain: 'example.com', url: 'https://profile.example.com/alice') } - let(:field) { Account::Field.new(account, 'name' => 'Website', 'value' => 'example.com') } - - before do - stub_request(:get, 'http://example.com').to_return(status: 200, body: html) - subject.call(field) - end - - context 'when a link contains an back' do - let(:html) do - <<-HTML - - - Follow me on Mastodon - - HTML - end - - it 'marks the field as verified' do - expect(field.verified?).to be true - end - end - - context 'when the link contains a link with a missing protocol slash' do - # This was seen in the wild where a user had three pages: - # 1. their mastodon profile, which linked to github and the personal website - # 2. their personal website correctly linking back to mastodon - # 3. a github profile that was linking to the personal website, but with - # a malformed protocol of http:/ - # - # This caused link verification between the mastodon profile and the - # website to fail. - # - # apparently github allows the user to enter website URLs with a single - # slash and makes no attempts to correct that. - let(:html) do - <<-HTML - Hello - HTML - end - - it 'does not crash' do - # We could probably put more effort into perhaps auto-correcting the - # link and following it anyway, but at the very least we shouldn't let - # exceptions bubble up - expect(field.verified?).to be false - end - end - end -end diff --git a/spec/services/vote_service_spec.rb b/spec/services/vote_service_spec.rb deleted file mode 100644 index 88207b001c..0000000000 --- a/spec/services/vote_service_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe VoteService do - describe '#call' do - subject { described_class.new.call(voter, poll, [0]) } - - context 'with a poll and poll options' do - let(:poll) { Fabricate(:poll, account: account, options: %w(Fun UnFun)) } - let(:fun_vote) { Fabricate(:poll_vote, poll: poll) } - let(:not_fun_vote) { Fabricate(:poll_vote, poll: poll) } - let(:voter) { Fabricate(:account, domain: nil) } - - context 'when the poll was created by a local account' do - let(:account) { Fabricate(:account, domain: nil) } - - it 'stores the votes and distributes the poll' do - expect { subject } - .to change(PollVote, :count).by(1) - - expect(ActivityPub::DistributePollUpdateWorker) - .to have_enqueued_sidekiq_job(poll.status.id) - end - end - - context 'when the poll was created by a remote account' do - let(:account) { Fabricate(:account, domain: 'host.example') } - - it 'stores the votes and processes delivery' do - expect { subject } - .to change(PollVote, :count).by(1) - - expect(ActivityPub::DeliveryWorker) - .to have_enqueued_sidekiq_job(anything, voter.id, poll.account.inbox_url) - end - end - end - end -end diff --git a/spec/services/webhook_service_spec.rb b/spec/services/webhook_service_spec.rb deleted file mode 100644 index 22a60db9f5..0000000000 --- a/spec/services/webhook_service_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe WebhookService do - describe '#call' do - context 'with a relevant event webhook' do - let!(:report) { Fabricate(:report) } - let!(:webhook) { Fabricate(:webhook, events: ['report.created']) } - - it 'finds and delivers webhook payloads' do - expect { subject.call('report.created', report) } - .to enqueue_sidekiq_job(Webhooks::DeliveryWorker) - .with( - webhook.id, - anything - ) - end - end - - context 'without any relevant event webhooks' do - let!(:report) { Fabricate(:report) } - - it 'does not deliver webhook payloads' do - expect { subject.call('report.created', report) } - .to_not enqueue_sidekiq_job(Webhooks::DeliveryWorker) - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 1f9cc40f12..0000000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -RSpec.configure do |config| - config.example_status_persistence_file_path = 'tmp/rspec/examples.txt' - config.expect_with :rspec do |expectations| - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - end - - config.mock_with :rspec do |mocks| - mocks.verify_partial_doubles = true - - config.around(:example, :without_verify_partial_doubles) do |example| - mocks.verify_partial_doubles = false - example.call - mocks.verify_partial_doubles = true - end - end - - config.before :suite do - Rails.application.load_seed - Chewy.strategy(:bypass) - - # NOTE: we switched registrations mode to closed by default, but the specs - # very heavily rely on having it enabled by default, as it relies on users - # being approved by default except in select cases where explicitly testing - # other registration modes - Setting.registrations_mode = 'open' - end - - config.after :suite do - FileUtils.rm_rf(Dir[Rails.root.join('spec', 'test_files')]) - end - - # Use the GitHub Annotations formatter for CI - if ENV['GITHUB_ACTIONS'] == 'true' && ENV['GITHUB_RSPEC'] == 'true' - require 'rspec/github' - config.add_formatter RSpec::Github::Formatter - end -end - -def body_as_json - json_str_to_hash(response.body) -end - -def json_str_to_hash(str) - JSON.parse(str, symbolize_names: true) -end - -def serialized_record_json(record, serializer, adapter: nil) - options = { serializer: serializer } - options[:adapter] = adapter if adapter.present? - JSON.parse( - ActiveModelSerializers::SerializableResource.new( - record, - options - ).to_json - ) -end - -def expect_push_bulk_to_match(klass, matcher) - allow(Sidekiq::Client).to receive(:push_bulk) - yield - expect(Sidekiq::Client).to have_received(:push_bulk).with(hash_including({ - 'class' => klass, - 'args' => matcher, - })) -end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb deleted file mode 100644 index be1378ffac..0000000000 --- a/spec/support/capybara.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -Capybara.server_host = 'localhost' -Capybara.server_port = 3000 -Capybara.app_host = "http://#{Capybara.server_host}:#{Capybara.server_port}" - -require 'selenium/webdriver' - -Capybara.register_driver :chrome do |app| - Capybara::Selenium::Driver.new(app, browser: :chrome) -end - -Capybara.register_driver :headless_chrome do |app| - options = Selenium::WebDriver::Chrome::Options.new - options.add_argument '--headless=new' - options.add_argument '--window-size=1680,1050' - - Capybara::Selenium::Driver.new( - app, - browser: :chrome, - options: options - ) -end - -Capybara.javascript_driver = :headless_chrome - -RSpec.configure do |config| - config.before(:each, type: :system) do - driven_by :rack_test - end - - config.before(:each, :js, type: :system) do - driven_by Capybara.javascript_driver - end -end diff --git a/spec/support/command_line_helpers.rb b/spec/support/command_line_helpers.rb deleted file mode 100644 index 6f9d63d939..0000000000 --- a/spec/support/command_line_helpers.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module CommandLineHelpers - def output_results(*args) - output( - include(*args) - ).to_stdout - end -end diff --git a/spec/support/examples/api.rb b/spec/support/examples/api.rb deleted file mode 100644 index d531860abf..0000000000 --- a/spec/support/examples/api.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -shared_examples 'forbidden for wrong scope' do |wrong_scope| - let(:scopes) { wrong_scope } - - it 'returns http forbidden' do - # Some examples have a subject which needs to be called to make a request - subject if request.nil? - - expect(response).to have_http_status(403) - end -end - -shared_examples 'forbidden for wrong role' do |wrong_role| - let(:role) { UserRole.find_by(name: wrong_role) } - - it 'returns http forbidden' do - # Some examples have a subject which needs to be called to make a request - subject if request.nil? - - expect(response).to have_http_status(403) - end -end diff --git a/spec/support/examples/cache.rb b/spec/support/examples/cache.rb deleted file mode 100644 index 60e522f426..0000000000 --- a/spec/support/examples/cache.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -shared_examples 'cacheable response' do |expects_vary: false| - it 'sets correct cache and vary headers and does not set cookies or session', :aggregate_failures do - expect(response.cookies).to be_empty - expect(response.headers['Set-Cookies']).to be_nil - - expect(session).to be_empty - - expect(response.headers['Vary']).to include(expects_vary) if expects_vary - - expect(response.headers['Cache-Control']).to include('public') - end -end diff --git a/spec/support/examples/cli.rb b/spec/support/examples/cli.rb deleted file mode 100644 index 091c842bd1..0000000000 --- a/spec/support/examples/cli.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -shared_examples 'CLI Command' do - it 'configures Thor to exit on failure' do - expect(described_class.exit_on_failure?).to be true - end - - it 'descends from the CLI base class' do - expect(described_class.new).to be_a(Mastodon::CLI::Base) - end -end diff --git a/spec/support/examples/lib/admin/checks.rb b/spec/support/examples/lib/admin/checks.rb deleted file mode 100644 index b50faa77ba..0000000000 --- a/spec/support/examples/lib/admin/checks.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -shared_examples 'a check available to devops users' do - describe 'skip?' do - context 'when user can view devops' do - before { allow(user).to receive(:can?).with(:view_devops).and_return(true) } - - it 'returns false' do - expect(check.skip?).to be false - end - end - - context 'when user cannot view devops' do - before { allow(user).to receive(:can?).with(:view_devops).and_return(false) } - - it 'returns true' do - expect(check.skip?).to be true - end - end - end -end diff --git a/spec/support/examples/mailers.rb b/spec/support/examples/mailers.rb deleted file mode 100644 index 213e873b4e..0000000000 --- a/spec/support/examples/mailers.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -shared_examples 'localized subject' do |*args, **kwrest| - it 'renders subject localized for the locale of the receiver' do - locale = :de - receiver.update!(locale: locale) - expect(mail.subject).to eq I18n.t(*args, **kwrest.merge(locale: locale)) - end - - it 'renders subject localized for the default locale if the locale of the receiver is unavailable' do - receiver.update!(locale: nil) - expect(mail.subject).to eq I18n.t(*args, **kwrest.merge(locale: I18n.default_locale)) - end -end diff --git a/spec/support/examples/models/concerns/account_avatar.rb b/spec/support/examples/models/concerns/account_avatar.rb deleted file mode 100644 index ab6020d834..0000000000 --- a/spec/support/examples/models/concerns/account_avatar.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -shared_examples 'AccountAvatar' do |fabricator| - describe 'static avatars', :attachment_processing do - describe 'when GIF' do - it 'creates a png static style' do - account = Fabricate(fabricator, avatar: attachment_fixture('avatar.gif')) - expect(account.avatar_static_url).to_not eq account.avatar_original_url - end - end - - describe 'when non-GIF' do - it 'does not create extra static style' do - account = Fabricate(fabricator, avatar: attachment_fixture('attachment.jpg')) - expect(account.avatar_static_url).to eq account.avatar_original_url - end - end - end - - describe 'base64-encoded files', :attachment_processing do - let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" } - let(:account) { Fabricate(fabricator, avatar: base64_attachment) } - - it 'saves avatar' do - expect(account.persisted?).to be true - expect(account.avatar).to_not be_nil - end - - it 'gives the avatar a file name' do - expect(account.avatar_file_name).to_not be_blank - end - - it 'saves a new avatar under a different file name' do - previous_file_name = account.avatar_file_name - account.update(avatar: base64_attachment) - expect(account.avatar_file_name).to_not eq previous_file_name - end - end -end diff --git a/spec/support/examples/models/concerns/account_header.rb b/spec/support/examples/models/concerns/account_header.rb deleted file mode 100644 index 43bbdaacf4..0000000000 --- a/spec/support/examples/models/concerns/account_header.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -shared_examples 'AccountHeader' do |fabricator| - describe 'base64-encoded files', :attachment_processing do - let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" } - let(:account) { Fabricate(fabricator, header: base64_attachment) } - - it 'saves header' do - expect(account.persisted?).to be true - expect(account.header).to_not be_nil - end - - it 'gives the header a file name' do - expect(account.header_file_name).to_not be_blank - end - - it 'saves a new header under a different file name' do - previous_file_name = account.header_file_name - account.update(header: base64_attachment) - expect(account.header_file_name).to_not eq previous_file_name - end - end -end diff --git a/spec/support/javascript_errors.rb b/spec/support/javascript_errors.rb deleted file mode 100644 index ef5945f37d..0000000000 --- a/spec/support/javascript_errors.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -RSpec.configure do |config| - config.after(:each, :js, type: :system) do - # Classes of intermittent ignorable errors - ignored_errors = [ - /Error while trying to use the following icon from the Manifest/, # https://github.com/mastodon/mastodon/pull/30793 - /Manifest: Line: 1, column: 1, Syntax error/, # Similar parsing/interruption issue as above - ] - errors = page.driver.browser.logs.get(:browser).reject do |error| - ignored_errors.any? { |pattern| pattern.match(error.message) } - end - - if errors.present? - aggregate_failures 'javascript errrors' do - errors.each do |error| - expect(error.level).to_not eq('SEVERE'), error.message - next unless error.level == 'WARNING' - - warn 'WARN: javascript warning' - warn error.message - end - end - end - end -end diff --git a/spec/support/matchers/api_pagination.rb b/spec/support/matchers/api_pagination.rb deleted file mode 100644 index f7d552b242..0000000000 --- a/spec/support/matchers/api_pagination.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -RSpec::Matchers.define :include_pagination_headers do |links| - match do |response| - links.map do |key, value| - response.headers['Link'].find_link(['rel', key.to_s]).href == value - end.all? - end - - failure_message do |response| - "expected that #{response.headers['Link']} would have the same values as #{links}." - end -end diff --git a/spec/support/matchers/json/match_json_schema.rb b/spec/support/matchers/json/match_json_schema.rb deleted file mode 100644 index b4ced8addb..0000000000 --- a/spec/support/matchers/json/match_json_schema.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -RSpec::Matchers.define :match_json_schema do |schema| - match do |input_json| - schema_path = Rails.root.join('spec', 'support', 'schema', "#{schema}.json").to_s - JSON::Validator.validate(schema_path, input_json, validate_schema: true) - end -end - -RSpec::Matchers.define :match_json_values do |values| - match do |string| - expect(json_str_to_hash(string)) - .to include(values) - end - - failure_message do |value| - "expected that #{value} would have the same values as #{values}." - end -end diff --git a/spec/support/matchers/model/model_have_error_on_field.rb b/spec/support/matchers/model/model_have_error_on_field.rb deleted file mode 100644 index 0f9c81a475..0000000000 --- a/spec/support/matchers/model/model_have_error_on_field.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -RSpec::Matchers.define :model_have_error_on_field do |expected| - match do |record| - record.valid? if record.errors.empty? - - record.errors.key?(expected) - end - - failure_message do |record| - keys = record.errors.attribute_names - - "expect record.errors(#{keys}) to include #{expected}" - end -end diff --git a/spec/support/omniauth_mocks.rb b/spec/support/omniauth_mocks.rb deleted file mode 100644 index 9883adec7a..0000000000 --- a/spec/support/omniauth_mocks.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -OmniAuth.config.test_mode = true - -def mock_omniauth(provider, data) - OmniAuth.config.mock_auth[provider] = OmniAuth::AuthHash.new(data) -end diff --git a/spec/support/schema/nodeinfo_2.0.json b/spec/support/schema/nodeinfo_2.0.json deleted file mode 100644 index 085ce542bd..0000000000 --- a/spec/support/schema/nodeinfo_2.0.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "http://nodeinfo.diaspora.software/ns/schema/2.0#", - "description": "NodeInfo schema version 2.0.", - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "software", - "protocols", - "services", - "openRegistrations", - "usage", - "metadata" - ], - "properties": { - "version": { - "description": "The schema version, must be 2.0.", - "enum": ["2.0"] - }, - "software": { - "description": "Metadata about server software in use.", - "type": "object", - "additionalProperties": false, - "required": ["name", "version"], - "properties": { - "name": { - "description": "The canonical name of this server software.", - "type": "string", - "pattern": "^[a-z0-9-]+$" - }, - "version": { - "description": "The version of this server software.", - "type": "string" - } - } - }, - "protocols": { - "description": "The protocols supported on this server.", - "type": "array", - "minItems": 1, - "items": { - "enum": [ - "activitypub", - "buddycloud", - "dfrn", - "diaspora", - "libertree", - "ostatus", - "pumpio", - "tent", - "xmpp", - "zot" - ] - } - }, - "services": { - "description": "The third party sites this server can connect to via their application API.", - "type": "object", - "additionalProperties": false, - "required": ["inbound", "outbound"], - "properties": { - "inbound": { - "description": "The third party sites this server can retrieve messages from for combined display with regular traffic.", - "type": "array", - "minItems": 0, - "items": { - "enum": [ - "atom1.0", - "gnusocial", - "imap", - "pnut", - "pop3", - "pumpio", - "rss2.0", - "twitter" - ] - } - }, - "outbound": { - "description": "The third party sites this server can publish messages to on the behalf of a user.", - "type": "array", - "minItems": 0, - "items": { - "enum": [ - "atom1.0", - "blogger", - "buddycloud", - "diaspora", - "dreamwidth", - "drupal", - "facebook", - "friendica", - "gnusocial", - "google", - "insanejournal", - "libertree", - "linkedin", - "livejournal", - "mediagoblin", - "myspace", - "pinterest", - "pnut", - "posterous", - "pumpio", - "redmatrix", - "rss2.0", - "smtp", - "tent", - "tumblr", - "twitter", - "wordpress", - "xmpp" - ] - } - } - } - }, - "openRegistrations": { - "description": "Whether this server allows open self-registration.", - "type": "boolean" - }, - "usage": { - "description": "Usage statistics for this server.", - "type": "object", - "additionalProperties": false, - "required": ["users"], - "properties": { - "users": { - "description": "statistics about the users of this server.", - "type": "object", - "additionalProperties": false, - "properties": { - "total": { - "description": "The total amount of on this server registered users.", - "type": "integer", - "minimum": 0 - }, - "activeHalfyear": { - "description": "The amount of users that signed in at least once in the last 180 days.", - "type": "integer", - "minimum": 0 - }, - "activeMonth": { - "description": "The amount of users that signed in at least once in the last 30 days.", - "type": "integer", - "minimum": 0 - } - } - }, - "localPosts": { - "description": "The amount of posts that were made by users that are registered on this server.", - "type": "integer", - "minimum": 0 - }, - "localComments": { - "description": "The amount of comments that were made by users that are registered on this server.", - "type": "integer", - "minimum": 0 - } - } - }, - "metadata": { - "description": "Free form key value pairs for software specific values. Clients should not rely on any specific key present.", - "type": "object", - "minProperties": 0, - "additionalProperties": true - } - } -} diff --git a/spec/support/search_data_manager.rb b/spec/support/search_data_manager.rb deleted file mode 100644 index 3c7140b48b..0000000000 --- a/spec/support/search_data_manager.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -class SearchDataManager - def prepare_test_data - 4.times do |i| - username = "search_test_account_#{i}" - account = Fabricate.create(:account, username: username, indexable: i.even?, discoverable: i.even?, note: "Lover of #{i}.") - 2.times do |j| - Fabricate.create(:status, account: account, text: "#{username}'s #{j} post", visibility: j.even? ? :public : :private) - end - end - - 3.times do |i| - Fabricate.create(:tag, name: "search_test_tag_#{i}") - end - end - - def indexes - [ - AccountsIndex, - PublicStatusesIndex, - StatusesIndex, - TagsIndex, - ] - end - - def populate_indexes - indexes.each do |index_class| - index_class.purge! - index_class.import! - end - end - - def remove_indexes - indexes.each(&:delete!) - end - - def cleanup_test_data - Status.destroy_all - Account.destroy_all - Tag.destroy_all - end -end - -RSpec.configure do |config| - config.before :suite do - if search_examples_present? - # Configure chewy to use `urgent` strategy to index documents - Chewy.strategy(:urgent) - - # Create search data - search_data_manager.prepare_test_data - end - end - - config.after :suite do - if search_examples_present? - # Clean up after search data - search_data_manager.cleanup_test_data - end - end - - config.around :each, :search do |example| - search_data_manager.populate_indexes - example.run - search_data_manager.remove_indexes - end - - private - - def search_data_manager - @search_data_manager ||= SearchDataManager.new - end - - def search_examples_present? - RSpec.world.filtered_examples.values.flatten.any? { |example| example.metadata[:search] == true } - end -end diff --git a/spec/support/signed_request_helpers.rb b/spec/support/signed_request_helpers.rb deleted file mode 100644 index 8a52179cae..0000000000 --- a/spec/support/signed_request_helpers.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module SignedRequestHelpers - def get(path, headers: nil, sign_with: nil, **args) - return super(path, headers: headers, **args) if sign_with.nil? - - headers ||= {} - headers['Date'] = Time.now.utc.httpdate - headers['Host'] = Rails.configuration.x.local_domain - signed_headers = headers.merge('(request-target)' => "get #{path}").slice('(request-target)', 'Host', 'Date') - - key_id = ActivityPub::TagManager.instance.key_uri_for(sign_with) - keypair = sign_with.keypair - signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") - signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) - - headers['Signature'] = "keyId=\"#{key_id}\",algorithm=\"rsa-sha256\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\"" - - super(path, headers: headers, **args) - end -end diff --git a/spec/support/stories/profile_stories.rb b/spec/support/stories/profile_stories.rb deleted file mode 100644 index 07eaaca9fb..0000000000 --- a/spec/support/stories/profile_stories.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -module ProfileStories - attr_reader :bob, :alice, :alice_bio - - def fill_in_auth_details(email, password) - fill_in 'user_email', with: email - fill_in 'user_password', with: password - click_on I18n.t('auth.login') - end - - def as_a_registered_user - @bob = Fabricate( - :user, - email: email, password: password, confirmed_at: confirmed_at, - account: Fabricate(:account, username: 'bob') - ) - - Web::Setting.where(user: bob).first_or_initialize(user: bob).update!(data: { introductionVersion: 2018_12_16_044202 }) if finished_onboarding - end - - def as_a_logged_in_user - as_a_registered_user - visit new_user_session_path - fill_in_auth_details(email, password) - end - - def as_a_logged_in_admin - # This is a bit awkward, but this avoids code duplication. - as_a_logged_in_user - bob.update!(role: UserRole.find_by!(name: 'Admin')) - end - - def with_alice_as_local_user - @alice_bio = '@alice and @bob are fictional characters commonly used as' \ - 'placeholder names in #cryptology, as well as #science and' \ - 'engineering 📖 literature. Not affiliated with @pepe.' - - @alice = Fabricate( - :user, - email: 'alice@example.com', password: password, confirmed_at: confirmed_at, - account: Fabricate(:account, username: 'alice', note: @alice_bio) - ) - end - - def confirmed_at - @confirmed_at ||= Time.zone.now - end - - def email - @email ||= 'test@example.com' - end - - def password - @password ||= 'password' - end - - def finished_onboarding - @finished_onboarding || false - end -end diff --git a/spec/support/streaming_server_manager.rb b/spec/support/streaming_server_manager.rb deleted file mode 100644 index 376d6b8725..0000000000 --- a/spec/support/streaming_server_manager.rb +++ /dev/null @@ -1,127 +0,0 @@ -# frozen_string_literal: true - -class StreamingServerManager - @running_thread = nil - - def initialize - at_exit { stop } - end - - def start(port: 4020) - return if @running_thread - - queue = Queue.new - - @queue = queue - - @running_thread = Thread.new do - Open3.popen2e( - { - 'REDIS_NAMESPACE' => ENV.fetch('REDIS_NAMESPACE'), - 'DB_NAME' => "#{ENV.fetch('DB_NAME', 'mastodon')}_test#{ENV.fetch('TEST_ENV_NUMBER', '')}", - 'RAILS_ENV' => ENV.fetch('RAILS_ENV', 'test'), - 'NODE_ENV' => ENV.fetch('STREAMING_NODE_ENV', 'development'), - 'PORT' => port.to_s, - }, - 'node index.js', # must not call yarn here, otherwise it will fail because yarn does not send signals to its child process - chdir: Rails.root.join('streaming') - ) do |_stdin, stdout_err, process_thread| - status = :starting - - # Spawn a thread to listen on streaming server output - output_thread = Thread.new do - stdout_err.each_line do |line| - Rails.logger.info "Streaming server: #{line}" - - if status == :starting && line.match('Streaming API now listening on') - status = :started - @queue.enq 'started' - end - end - end - - # And another thread to listen on commands from the main thread - loop do - msg = queue.pop - - case msg - when 'stop' - # we need to properly stop the reading thread - output_thread.kill - - # Then stop the node process - Process.kill('KILL', process_thread.pid) - - # And we stop ourselves - @running_thread.kill - end - end - end - end - - # wait for 10 seconds for the streaming server to start - Timeout.timeout(10) do - loop do - break if @queue.pop == 'started' - end - end - end - - def stop - return unless @running_thread - - @queue.enq 'stop' - - # Wait for the thread to end - @running_thread.join - end -end - -RSpec.configure do |config| - config.before :suite do - if streaming_examples_present? - # Start the node streaming server - streaming_server_manager.start(port: STREAMING_PORT) - end - end - - config.after :suite do - if streaming_examples_present? - # Stop the node streaming server - streaming_server_manager.stop - end - end - - config.around :each, :streaming, type: :system do |example| - # Streaming server needs DB access but `use_transactional_tests` rolls back - # every transaction. Disable this feature for streaming tests, and use - # DatabaseCleaner to clean the database tables between each test. - self.use_transactional_tests = false - - DatabaseCleaner.cleaning do - # NOTE: we switched registrations mode to closed by default, but the specs - # very heavily rely on having it enabled by default, as it relies on users - # being approved by default except in select cases where explicitly testing - # other registration modes - # Also needs to be set per-example here because of the database cleaner. - Setting.registrations_mode = 'open' - - # Load seeds so we have the default roles otherwise cleared by `DatabaseCleaner` - Rails.application.load_seed - - example.run - end - - self.use_transactional_tests = true - end - - private - - def streaming_server_manager - @streaming_server_manager ||= StreamingServerManager.new - end - - def streaming_examples_present? - RSpec.world.filtered_examples.values.flatten.any? { |example| example.metadata[:streaming] == true } - end -end diff --git a/spec/support/threading_helpers.rb b/spec/support/threading_helpers.rb deleted file mode 100644 index edf45822ca..0000000000 --- a/spec/support/threading_helpers.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module ThreadingHelpers - def multi_threaded_execution(thread_count) - wait_for_start = true - - threads = Array.new(thread_count) do - Thread.new do - true while wait_for_start - yield - end - end - - wait_for_start = false - threads.each(&:join) - end -end diff --git a/spec/system/admin/accounts_spec.rb b/spec/system/admin/accounts_spec.rb deleted file mode 100644 index 20813f6be4..0000000000 --- a/spec/system/admin/accounts_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Admin::Accounts' do - let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - - before do - sign_in current_user - end - - describe 'Performing batch updates' do - let(:unapproved_user_account) { Fabricate(:account) } - let(:approved_user_account) { Fabricate(:account) } - - before do - unapproved_user_account.user.update(approved: false) - approved_user_account.user.update(approved: true) - - visit admin_accounts_path - end - - context 'without selecting any accounts' do - it 'displays a notice about account selection' do - click_on button_for_suspend - - expect(page).to have_content(selection_error_text) - end - end - - context 'with action of `suspend`' do - it 'suspends the account' do - batch_checkbox_for(approved_user_account).check - - click_on button_for_suspend - - expect(approved_user_account.reload).to be_suspended - end - end - - context 'with action of `approve`' do - it 'approves the account user' do - batch_checkbox_for(unapproved_user_account).check - - click_on button_for_approve - - expect(unapproved_user_account.reload.user).to be_approved - end - end - - context 'with action of `reject`', :inline_jobs do - it 'rejects and removes the account' do - batch_checkbox_for(unapproved_user_account).check - - click_on button_for_reject - - expect { unapproved_user_account.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - def button_for_suspend - I18n.t('admin.accounts.perform_full_suspension') - end - - def button_for_approve - I18n.t('admin.accounts.approve') - end - - def button_for_reject - I18n.t('admin.accounts.reject') - end - - def selection_error_text - I18n.t('admin.accounts.no_account_selected') - end - - def batch_checkbox_for(account) - find("#form_account_batch_account_ids_#{account.id}") - end - end -end diff --git a/spec/system/admin/custom_emojis_spec.rb b/spec/system/admin/custom_emojis_spec.rb deleted file mode 100644 index 8a8b6efcd1..0000000000 --- a/spec/system/admin/custom_emojis_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Admin::CustomEmojis' do - let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - - before do - sign_in current_user - end - - describe 'Performing batch updates' do - before do - visit admin_custom_emojis_path - end - - context 'without selecting any records' do - it 'displays a notice about selection' do - click_on button_for_enable - - expect(page).to have_content(selection_error_text) - end - end - - def button_for_enable - I18n.t('admin.custom_emojis.enable') - end - - def selection_error_text - I18n.t('admin.custom_emojis.no_emoji_selected') - end - end -end diff --git a/spec/system/admin/domain_blocks_spec.rb b/spec/system/admin/domain_blocks_spec.rb deleted file mode 100644 index 99aa7cf1a7..0000000000 --- a/spec/system/admin/domain_blocks_spec.rb +++ /dev/null @@ -1,115 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'blocking domains through the moderation interface' do - before do - allow(DomainBlockWorker).to receive(:perform_async).and_return(true) - sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user - end - - context 'when silencing a new domain' do - it 'adds a new domain block' do - visit new_admin_domain_block_path - - submit_domain_block('example.com', 'silence') - - expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be true - expect(DomainBlockWorker).to have_received(:perform_async) - end - end - - context 'when suspending a new domain' do - it 'presents a confirmation screen before suspending the domain' do - visit new_admin_domain_block_path - - submit_domain_block('example.com', 'suspend') - - # It doesn't immediately block but presents a confirmation screen - expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) - expect(DomainBlockWorker).to_not have_received(:perform_async) - - # Confirming creates a block - click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') - - expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true - expect(DomainBlockWorker).to have_received(:perform_async) - end - end - - context 'when suspending a domain that is already silenced' do - it 'presents a confirmation screen before suspending the domain' do - domain_block = Fabricate(:domain_block, domain: 'example.com', severity: 'silence') - - visit new_admin_domain_block_path - - submit_domain_block('example.com', 'suspend') - - # It doesn't immediately block but presents a confirmation screen - expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) - expect(DomainBlockWorker).to_not have_received(:perform_async) - - # Confirming updates the block - click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') - - expect(domain_block.reload.severity).to eq 'suspend' - expect(DomainBlockWorker).to have_received(:perform_async) - end - end - - context 'when suspending a subdomain of an already-silenced domain' do - it 'presents a confirmation screen before suspending the domain' do - domain_block = Fabricate(:domain_block, domain: 'example.com', severity: 'silence') - - visit new_admin_domain_block_path - - submit_domain_block('subdomain.example.com', 'suspend') - - # It doesn't immediately block but presents a confirmation screen - expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'subdomain.example.com')) - expect(DomainBlockWorker).to_not have_received(:perform_async) - - # Confirming creates the block - click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') - - expect(DomainBlock.where(domain: 'subdomain.example.com', severity: 'suspend')).to exist - expect(DomainBlockWorker).to have_received(:perform_async) - - # And leaves the previous block alone - expect(domain_block.reload) - .to have_attributes( - severity: eq('silence'), - domain: eq('example.com') - ) - end - end - - context 'when editing a domain block' do - it 'presents a confirmation screen before suspending the domain' do - domain_block = Fabricate(:domain_block, domain: 'example.com', severity: 'silence') - - visit edit_admin_domain_block_path(domain_block) - - select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_on I18n.t('generic.save_changes') - - # It doesn't immediately block but presents a confirmation screen - expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) - expect(DomainBlockWorker).to_not have_received(:perform_async) - - # Confirming updates the block - click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') - expect(DomainBlockWorker).to have_received(:perform_async) - - expect(domain_block.reload.severity).to eq 'suspend' - end - end - - private - - def submit_domain_block(domain, severity) - fill_in 'domain_block_domain', with: domain - select I18n.t("admin.domain_blocks.new.severity.#{severity}"), from: 'domain_block_severity' - click_on I18n.t('admin.domain_blocks.new.create') - end -end diff --git a/spec/system/admin/email_domain_blocks_spec.rb b/spec/system/admin/email_domain_blocks_spec.rb deleted file mode 100644 index 14959cbe74..0000000000 --- a/spec/system/admin/email_domain_blocks_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Admin::EmailDomainBlocks' do - let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - - before do - sign_in current_user - end - - describe 'Performing batch updates' do - before do - visit admin_email_domain_blocks_path - end - - context 'without selecting any records' do - it 'displays a notice about selection' do - click_on button_for_delete - - expect(page).to have_content(selection_error_text) - end - end - - def button_for_delete - I18n.t('admin.email_domain_blocks.delete') - end - - def selection_error_text - I18n.t('admin.email_domain_blocks.no_email_domain_block_selected') - end - end -end diff --git a/spec/system/admin/ip_blocks_spec.rb b/spec/system/admin/ip_blocks_spec.rb deleted file mode 100644 index c9b16f6f78..0000000000 --- a/spec/system/admin/ip_blocks_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Admin::IpBlocks' do - let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - - before do - sign_in current_user - end - - describe 'Performing batch updates' do - before do - visit admin_ip_blocks_path - end - - context 'without selecting any records' do - it 'displays a notice about selection' do - click_on button_for_delete - - expect(page).to have_content(selection_error_text) - end - end - - def button_for_delete - I18n.t('admin.ip_blocks.delete') - end - - def selection_error_text - I18n.t('admin.ip_blocks.no_ip_block_selected') - end - end -end diff --git a/spec/system/admin/software_updates_spec.rb b/spec/system/admin/software_updates_spec.rb deleted file mode 100644 index 4a635d1a79..0000000000 --- a/spec/system/admin/software_updates_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'finding software updates through the admin interface' do - before do - Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: true, release_notes: 'https://github.com/mastodon/mastodon/releases/v99') - - sign_in Fabricate(:user, role: UserRole.find_by(name: 'Owner')), scope: :user - end - - it 'shows a link to the software updates page, which links to release notes' do - visit settings_profile_path - click_on I18n.t('admin.critical_update_pending') - - expect(page).to have_title(I18n.t('admin.software_updates.title')) - - expect(page).to have_content('99.99.99') - - click_on I18n.t('admin.software_updates.release_notes') - expect(page).to have_current_path('https://github.com/mastodon/mastodon/releases/v99', url: true) - end -end diff --git a/spec/system/admin/statuses_spec.rb b/spec/system/admin/statuses_spec.rb deleted file mode 100644 index 531d0de953..0000000000 --- a/spec/system/admin/statuses_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Admin::Statuses' do - let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - - before do - sign_in current_user - end - - describe 'Performing batch updates' do - before do - _status = Fabricate(:status, account: current_user.account) - visit admin_account_statuses_path(account_id: current_user.account_id) - end - - context 'without selecting any records' do - it 'displays a notice about selection' do - click_on button_for_report - - expect(page).to have_content(selection_error_text) - end - end - - def button_for_report - I18n.t('admin.statuses.batch.report') - end - - def selection_error_text - I18n.t('admin.statuses.no_status_selected') - end - end -end diff --git a/spec/system/admin/trends/links/preview_card_providers_spec.rb b/spec/system/admin/trends/links/preview_card_providers_spec.rb deleted file mode 100644 index dca89117b1..0000000000 --- a/spec/system/admin/trends/links/preview_card_providers_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Admin::Trends::Links::PreviewCardProviders' do - let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - - before do - sign_in current_user - end - - describe 'Performing batch updates' do - before do - visit admin_trends_links_preview_card_providers_path - end - - context 'without selecting any records' do - it 'displays a notice about selection' do - click_on button_for_allow - - expect(page).to have_content(selection_error_text) - end - end - - def button_for_allow - I18n.t('admin.trends.allow') - end - - def selection_error_text - I18n.t('admin.trends.links.publishers.no_publisher_selected') - end - end -end diff --git a/spec/system/admin/trends/links_spec.rb b/spec/system/admin/trends/links_spec.rb deleted file mode 100644 index 99638bc069..0000000000 --- a/spec/system/admin/trends/links_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Admin::Trends::Links' do - let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - - before do - sign_in current_user - end - - describe 'Performing batch updates' do - before do - visit admin_trends_links_path - end - - context 'without selecting any records' do - it 'displays a notice about selection' do - click_on button_for_allow - - expect(page).to have_content(selection_error_text) - end - end - - def button_for_allow - I18n.t('admin.trends.links.allow') - end - - def selection_error_text - I18n.t('admin.trends.links.no_link_selected') - end - end -end diff --git a/spec/system/admin/trends/statuses_spec.rb b/spec/system/admin/trends/statuses_spec.rb deleted file mode 100644 index 779a15d38f..0000000000 --- a/spec/system/admin/trends/statuses_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Admin::Trends::Statuses' do - let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - - before do - sign_in current_user - end - - describe 'Performing batch updates' do - before do - visit admin_trends_statuses_path - end - - context 'without selecting any records' do - it 'displays a notice about selection' do - click_on button_for_allow - - expect(page).to have_content(selection_error_text) - end - end - - def button_for_allow - I18n.t('admin.trends.statuses.allow') - end - - def selection_error_text - I18n.t('admin.trends.statuses.no_status_selected') - end - end -end diff --git a/spec/system/admin/trends/tags_spec.rb b/spec/system/admin/trends/tags_spec.rb deleted file mode 100644 index 52e49c3a5d..0000000000 --- a/spec/system/admin/trends/tags_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Admin::Trends::Tags' do - let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } - - before do - sign_in current_user - end - - describe 'Performing batch updates' do - before do - visit admin_trends_tags_path - end - - context 'without selecting any records' do - it 'displays a notice about selection' do - click_on button_for_allow - - expect(page).to have_content(selection_error_text) - end - end - - def button_for_allow - I18n.t('admin.trends.allow') - end - - def selection_error_text - I18n.t('admin.trends.tags.no_tag_selected') - end - end -end diff --git a/spec/system/captcha_spec.rb b/spec/system/captcha_spec.rb deleted file mode 100644 index 06c823adf2..0000000000 --- a/spec/system/captcha_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'email confirmation flow when captcha is enabled' do - let(:user) { Fabricate(:user, confirmed_at: nil, confirmation_token: 'foobar', created_by_application: client_app) } - let(:client_app) { nil } - - before do - allow(Auth::ConfirmationsController).to receive(:new).and_return(stubbed_controller) - end - - context 'when the user signed up through an app' do - let(:client_app) { Fabricate(:application) } - - it 'logs in' do - visit "/auth/confirmation?confirmation_token=#{user.confirmation_token}&redirect_to_app=true" - - # It presents the user with a captcha form - expect(page).to have_title(I18n.t('auth.captcha_confirmation.title')) - - # It redirects to app and confirms user - expect { click_on I18n.t('challenge.confirm') } - .to change { user.reload.confirmed? }.from(false).to(true) - - expect(page).to have_current_path(/\A#{client_app.confirmation_redirect_uri}/, url: true) - - # Browsers will generally reload the original page upon redirection - # to external handlers, so test this as well - visit "/auth/confirmation?confirmation_token=#{user.confirmation_token}&redirect_to_app=true" - - # It presents a page with a link to the app callback - expect(page) - .to have_content(I18n.t('auth.confirmations.registration_complete', domain: 'cb6e6126.ngrok.io')) - .and have_link(I18n.t('auth.confirmations.clicking_this_link'), href: client_app.confirmation_redirect_uri) - end - end - - private - - def stubbed_controller - Auth::ConfirmationsController.new.tap do |controller| - allow(controller).to receive_messages(captcha_enabled?: true, check_captcha!: true, render_captcha: nil) - end - end -end diff --git a/spec/system/filters_spec.rb b/spec/system/filters_spec.rb deleted file mode 100644 index a0cb965a61..0000000000 --- a/spec/system/filters_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Filters' do - let(:user) { Fabricate(:user) } - let(:filter_title) { 'Filter of fun and games' } - - before { sign_in(user) } - - describe 'Creating a filter' do - it 'Populates a new filter from form' do - navigate_to_filters - - click_on I18n.t('filters.new.title') - fill_in_filter_form - expect(page).to have_content(filter_title) - end - end - - describe 'Editing an existing filter' do - let(:new_title) { 'Change title value' } - - before { Fabricate :custom_filter, account: user.account, title: filter_title } - - it 'Updates the saved filter' do - navigate_to_filters - - click_on filter_title - - fill_in filter_title_field, with: new_title - click_on I18n.t('generic.save_changes') - - expect(page).to have_content(new_title) - end - end - - describe 'Destroying an existing filter' do - before { Fabricate :custom_filter, account: user.account, title: filter_title } - - it 'Deletes the filter' do - navigate_to_filters - - expect(page).to have_content filter_title - expect do - click_on I18n.t('filters.index.delete') - end.to change(CustomFilter, :count).by(-1) - - expect(page).to have_no_content(filter_title) - end - end - - def navigate_to_filters - visit settings_path - - click_on I18n.t('filters.index.title') - expect(page).to have_content I18n.t('filters.index.title') - end - - def fill_in_filter_form - fill_in filter_title_field, with: filter_title - check I18n.t('filters.contexts.home') - within('.custom_filter_keywords_keyword') do - fill_in with: 'Keyword' - end - click_on I18n.t('filters.new.save') - end - - def filter_title_field - I18n.t('simple_form.labels.defaults.title') - end -end diff --git a/spec/system/log_in_spec.rb b/spec/system/log_in_spec.rb deleted file mode 100644 index 8a73c42d2e..0000000000 --- a/spec/system/log_in_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Log in' do - include ProfileStories - - subject { page } - - let(:email) { 'test@example.com' } - let(:password) { 'password' } - let(:confirmed_at) { Time.zone.now } - - before do - as_a_registered_user - visit new_user_session_path - end - - it 'A valid email and password user is able to log in' do - fill_in_auth_details(email, password) - - expect(subject).to have_css('div.app-holder') - end - - it 'A invalid email and password user is not able to log in' do - fill_in_auth_details('invalid_email', 'invalid_password') - - expect(subject).to have_css('.flash-message', text: failure_message('invalid')) - end - - context 'when confirmed at is nil' do - let(:confirmed_at) { nil } - - it 'A unconfirmed user is able to log in' do - fill_in_auth_details(email, password) - - expect(subject).to have_css('div.admin-wrapper') - end - end - - def failure_message(message) - keys = User.authentication_keys.map { |key| User.human_attribute_name(key) } - I18n.t("devise.failure.#{message}", authentication_keys: keys.join('support.array.words_connector')) - end -end diff --git a/spec/system/new_statuses_spec.rb b/spec/system/new_statuses_spec.rb deleted file mode 100644 index 2f2fcf2248..0000000000 --- a/spec/system/new_statuses_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'NewStatuses', :inline_jobs, :js, :streaming do - include ProfileStories - - subject { page } - - let(:email) { 'test@example.com' } - let(:password) { 'password' } - let(:confirmed_at) { Time.zone.now } - let(:finished_onboarding) { true } - - before do - as_a_logged_in_user - visit root_path - end - - it 'can be posted' do - expect(subject).to have_css('div.app-holder') - - status_text = 'This is a new status!' - - within('.compose-form') do - fill_in "What's on your mind?", with: status_text - click_on 'Post' - end - - expect(subject).to have_css('.status__content__text', text: status_text) - end - - it 'can be posted again' do - expect(subject).to have_css('div.app-holder') - - status_text = 'This is a second status!' - - within('.compose-form') do - fill_in "What's on your mind?", with: status_text - click_on 'Post' - end - - expect(subject).to have_css('.status__content__text', text: status_text) - end -end diff --git a/spec/system/oauth_spec.rb b/spec/system/oauth_spec.rb deleted file mode 100644 index 5d06f6111c..0000000000 --- a/spec/system/oauth_spec.rb +++ /dev/null @@ -1,255 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Using OAuth from an external app' do - include ProfileStories - - subject { visit "/oauth/authorize?#{params.to_query}" } - - let(:client_app) { Doorkeeper::Application.create!(name: 'test', redirect_uri: about_url(host: Rails.application.config.x.local_domain), scopes: 'read') } - let(:params) do - { client_id: client_app.uid, response_type: 'code', redirect_uri: client_app.redirect_uri, scope: 'read' } - end - - context 'when the user is already logged in' do - let!(:user) { Fabricate(:user) } - - before do - visit new_user_session_path - fill_in_auth_details(user.email, user.password) - end - - it 'when accepting the authorization request' do - subject - - # It presents the user with an authorization page - expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) - - # Upon authorizing, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.authorize') - expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) - - # It grants the app access to the account - expect(Doorkeeper::AccessGrant.exists?(application: client_app, resource_owner_id: user.id)).to be true - end - - it 'when rejecting the authorization request' do - subject - - # It presents the user with an authorization page - expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.deny')) - - # Upon denying, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.deny') - expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) - - # It does not grant the app access to the account - expect(Doorkeeper::AccessGrant.exists?(application: client_app, resource_owner_id: user.id)).to be false - end - - # The tests in this context ensures that requests without PKCE parameters - # still work; In the future we likely want to force usage of PKCE for - # security reasons, as per: - # - # https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-27.html#section-2.1.1-9 - context 'when not using PKCE' do - it 'does not include the PKCE values in the hidden inputs' do - subject - - code_challenge_inputs = all('.oauth-prompt input[name=code_challenge]', visible: false) - code_challenge_method_inputs = all('.oauth-prompt input[name=code_challenge_method]', visible: false) - - expect(code_challenge_inputs).to_not be_empty - expect(code_challenge_method_inputs).to_not be_empty - - (code_challenge_inputs.to_a + code_challenge_method_inputs.to_a).each do |input| - expect(input.value).to be_nil - end - end - end - - context 'when using PKCE' do - let(:params) do - { client_id: client_app.uid, response_type: 'code', redirect_uri: client_app.redirect_uri, scope: 'read', code_challenge_method: pkce_code_challenge_method, code_challenge: pkce_code_challenge } - end - let(:pkce_code_challenge) { SecureRandom.hex(32) } - let(:pkce_code_challenge_method) { 'S256' } - - context 'when using S256 code challenge method' do - it 'includes the PKCE values in the hidden inputs' do - subject - - code_challenge_inputs = all('.oauth-prompt input[name=code_challenge]', visible: false) - code_challenge_method_inputs = all('.oauth-prompt input[name=code_challenge_method]', visible: false) - - expect(code_challenge_inputs).to_not be_empty - expect(code_challenge_method_inputs).to_not be_empty - - code_challenge_inputs.each do |input| - expect(input.value).to eq pkce_code_challenge - end - code_challenge_method_inputs.each do |input| - expect(input.value).to eq pkce_code_challenge_method - end - end - end - - context 'when using plain code challenge method' do - let(:pkce_code_challenge_method) { 'plain' } - - it 'does not include the PKCE values in the response' do - subject - - expect(page).to have_no_css('.oauth-prompt input[name=code_challenge]') - expect(page).to have_no_css('.oauth-prompt input[name=code_challenge_method]') - end - - it 'does not include the authorize button' do - subject - - expect(page).to have_no_css('.oauth-prompt button[type="submit"]') - end - - it 'includes an error message' do - subject - - within '.form-container .flash-message' do - expect(page).to have_content(I18n.t('doorkeeper.errors.messages.invalid_code_challenge_method')) - end - end - end - end - end - - context 'when the user is not already logged in' do - let(:email) { 'test@example.com' } - let(:password) { 'testpassword' } - let(:user) { Fabricate(:user, email: email, password: password) } - - before do - user.mark_email_as_confirmed! - user.approve! - end - - it 'when accepting the authorization request' do - params = { client_id: client_app.uid, response_type: 'code', redirect_uri: client_app.redirect_uri, scope: 'read' } - visit "/oauth/authorize?#{params.to_query}" - - # It presents the user with a log-in page - expect(page).to have_content(I18n.t('auth.login')) - - # Failing to log-in presents the form again - fill_in_auth_details(email, 'wrong password') - expect(page).to have_content(I18n.t('auth.login')) - - # Logging in redirects to an authorization page - fill_in_auth_details(email, password) - expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) - - # Upon authorizing, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.authorize') - expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) - - # It grants the app access to the account - expect(Doorkeeper::AccessGrant.exists?(application: client_app, resource_owner_id: user.id)).to be true - end - - it 'when rejecting the authorization request' do - params = { client_id: client_app.uid, response_type: 'code', redirect_uri: client_app.redirect_uri, scope: 'read' } - visit "/oauth/authorize?#{params.to_query}" - - # It presents the user with a log-in page - expect(page).to have_content(I18n.t('auth.login')) - - # Failing to log-in presents the form again - fill_in_auth_details(email, 'wrong password') - expect(page).to have_content(I18n.t('auth.login')) - - # Logging in redirects to an authorization page - fill_in_auth_details(email, password) - expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) - - # Upon denying, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.deny') - expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) - - # It does not grant the app access to the account - expect(Doorkeeper::AccessGrant.exists?(application: client_app, resource_owner_id: user.id)).to be false - end - - context 'when the user has set up TOTP' do - let(:user) { Fabricate(:user, email: email, password: password, otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) } - - it 'when accepting the authorization request' do - params = { client_id: client_app.uid, response_type: 'code', redirect_uri: client_app.redirect_uri, scope: 'read' } - visit "/oauth/authorize?#{params.to_query}" - - # It presents the user with a log-in page - expect(page).to have_content(I18n.t('auth.login')) - - # Failing to log-in presents the form again - fill_in_auth_details(email, 'wrong password') - expect(page).to have_content(I18n.t('auth.login')) - - # Logging in redirects to a two-factor authentication page - fill_in_auth_details(email, password) - expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) - - # Filling in an incorrect two-factor authentication code presents the form again - fill_in_otp_details('wrong') - expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) - - # Filling in the correct TOTP code redirects to an app authorization page - fill_in_otp_details(user.current_otp) - expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) - - # Upon authorizing, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.authorize') - expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) - - # It grants the app access to the account - expect(Doorkeeper::AccessGrant.exists?(application: client_app, resource_owner_id: user.id)).to be true - end - - it 'when rejecting the authorization request' do - params = { client_id: client_app.uid, response_type: 'code', redirect_uri: client_app.redirect_uri, scope: 'read' } - visit "/oauth/authorize?#{params.to_query}" - - # It presents the user with a log-in page - expect(page).to have_content(I18n.t('auth.login')) - - # Failing to log-in presents the form again - fill_in_auth_details(email, 'wrong password') - expect(page).to have_content(I18n.t('auth.login')) - - # Logging in redirects to a two-factor authentication page - fill_in_auth_details(email, password) - expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) - - # Filling in an incorrect two-factor authentication code presents the form again - fill_in_otp_details('wrong') - expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) - - # Filling in the correct TOTP code redirects to an app authorization page - fill_in_otp_details(user.current_otp) - expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) - - # Upon denying, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.deny') - expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) - - # It does not grant the app access to the account - expect(Doorkeeper::AccessGrant.exists?(application: client_app, resource_owner_id: user.id)).to be false - end - end - # TODO: external auth - end - - private - - def fill_in_otp_details(value) - fill_in 'user_otp_attempt', with: value - click_on I18n.t('auth.login') - end -end diff --git a/spec/system/ocr_spec.rb b/spec/system/ocr_spec.rb deleted file mode 100644 index 17d18af158..0000000000 --- a/spec/system/ocr_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'OCR', :attachment_processing, :inline_jobs, :js, :streaming do - include ProfileStories - - let(:email) { 'test@example.com' } - let(:password) { 'password' } - let(:confirmed_at) { Time.zone.now } - let(:finished_onboarding) { true } - - before do - as_a_logged_in_user - visit root_path - end - - it 'can recognize text in a media attachment' do - expect(page).to have_css('div.app-holder') - - within('.compose-form') do - attach_file('file-upload-input', file_fixture('text.png'), make_visible: true) - - within('.compose-form__upload') do - click_on('Edit') - end - end - - click_on('Detect text from picture') - - expect(page).to have_css('#upload-modal__description', text: /Hello Mastodon\s*/, wait: 10) - end -end diff --git a/spec/system/profile_spec.rb b/spec/system/profile_spec.rb deleted file mode 100644 index 2517e823b5..0000000000 --- a/spec/system/profile_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Profile' do - include ProfileStories - - subject { page } - - let(:local_domain) { Rails.configuration.x.local_domain } - - before do - as_a_logged_in_user - with_alice_as_local_user - end - - it 'I can view Annes public account' do - visit account_path('alice') - - expect(subject).to have_title("alice (@alice@#{local_domain})") - end - - it 'I can change my account' do - visit settings_profile_path - - fill_in 'Display name', with: 'Bob' - fill_in 'Bio', with: 'Bob is silent' - - first('button[type=submit]').click - - expect(subject).to have_content 'Changes successfully saved!' - end -end diff --git a/spec/system/redirections_spec.rb b/spec/system/redirections_spec.rb deleted file mode 100644 index f73ab58470..0000000000 --- a/spec/system/redirections_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'redirection confirmations' do - let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/foo', url: 'https://example.com/@foo') } - let(:status) { Fabricate(:status, account: account, uri: 'https://example.com/users/foo/statuses/1', url: 'https://example.com/@foo/1') } - - context 'when a logged out user visits a local page for a remote account' do - it 'shows a confirmation page' do - visit "/@#{account.pretty_acct}" - - # It explains about the redirect - expect(page).to have_content(I18n.t('redirects.title', instance: 'cb6e6126.ngrok.io')) - - # It features an appropriate link - expect(page).to have_link(account.url, href: account.url) - end - end - - context 'when a logged out user visits a local page for a remote status' do - it 'shows a confirmation page' do - visit "/@#{account.pretty_acct}/#{status.id}" - - # It explains about the redirect - expect(page).to have_content(I18n.t('redirects.title', instance: 'cb6e6126.ngrok.io')) - - # It features an appropriate link - expect(page).to have_link(status.url, href: status.url) - end - end -end diff --git a/spec/system/report_interface_spec.rb b/spec/system/report_interface_spec.rb deleted file mode 100644 index e6cc3b1b68..0000000000 --- a/spec/system/report_interface_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'report interface', :attachment_processing, :js, :streaming do - include ProfileStories - - let(:email) { 'admin@example.com' } - let(:password) { 'password' } - let(:confirmed_at) { Time.zone.now } - let(:finished_onboarding) { true } - - let(:reported_account) { Fabricate(:account) } - let(:reported_status) { Fabricate(:status, account: reported_account) } - let(:media_attachment) { Fabricate(:media_attachment, account: reported_account, status: reported_status, file: attachment_fixture('attachment.jpg')) } - let!(:report) { Fabricate(:report, target_account: reported_account, status_ids: [media_attachment.status.id]) } - - before do - as_a_logged_in_admin - visit admin_report_path(report) - end - - it 'displays the report interface, including the javascript bits' do - # The report category selector React component is properly rendered - expect(page).to have_css('.report-reason-selector') - - # The media React component is properly rendered - page.scroll_to(page.find('.batch-table__row')) - expect(page).to have_css('.spoiler-button__overlay__label') - end -end diff --git a/spec/system/severed_relationships_spec.rb b/spec/system/severed_relationships_spec.rb deleted file mode 100644 index b933398a08..0000000000 --- a/spec/system/severed_relationships_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'Severed relationships page' do - include ProfileStories - - describe 'GET severed_relationships#index' do - before do - as_a_logged_in_user - - event = Fabricate(:relationship_severance_event) - Fabricate.times(3, :severed_relationship, local_account: bob.account, relationship_severance_event: event) - Fabricate(:account_relationship_severance_event, account: bob.account, relationship_severance_event: event) - end - - it 'returns http success' do - visit severed_relationships_path - - expect(page).to have_title(I18n.t('settings.severed_relationships')) - expect(page).to have_link(href: following_severed_relationship_path(AccountRelationshipSeveranceEvent.first, format: :csv)) - end - end -end diff --git a/spec/system/share_entrypoint_spec.rb b/spec/system/share_entrypoint_spec.rb deleted file mode 100644 index 5fdbeacefa..0000000000 --- a/spec/system/share_entrypoint_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'ShareEntrypoint', :js, :streaming do - include ProfileStories - - subject { page } - - let(:email) { 'test@example.com' } - let(:password) { 'password' } - let(:confirmed_at) { Time.zone.now } - let(:finished_onboarding) { true } - - before do - as_a_logged_in_user - visit share_path - end - - it 'can be used to post a new status' do - expect(subject).to have_css('div#mastodon-compose') - expect(subject).to have_css('.compose-form__submit') - - status_text = 'This is a new status!' - - within('.compose-form') do - fill_in "What's on your mind?", with: status_text - click_on 'Post' - end - - expect(subject).to have_css('.notification-bar-message', text: 'Post published.') - end -end diff --git a/spec/system/unlogged_spec.rb b/spec/system/unlogged_spec.rb deleted file mode 100644 index 417ccdaeb6..0000000000 --- a/spec/system/unlogged_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'UnloggedBrowsing', :js, :streaming do - subject { page } - - before do - visit root_path - end - - it 'loads the home page' do - expect(subject).to have_css('div.app-holder') - - expect(subject).to have_css('div.columns-area__panels__main') - end -end diff --git a/spec/validators/blacklisted_email_validator_spec.rb b/spec/validators/blacklisted_email_validator_spec.rb deleted file mode 100644 index 86760df2e7..0000000000 --- a/spec/validators/blacklisted_email_validator_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe BlacklistedEmailValidator do - describe '#validate' do - subject { described_class.new.validate(user) } - - let(:user) { instance_double(User, email: 'info@mail.com', sign_up_ip: '1.2.3.4', errors: errors) } - let(:errors) { instance_double(ActiveModel::Errors, add: nil) } - - before do - allow(user).to receive(:valid_invitation?).and_return(false) - allow(EmailDomainBlock).to receive(:block?) { blocked_email } - end - - context 'when e-mail provider is blocked' do - let(:blocked_email) { true } - - it 'adds error' do - subject - - expect(errors).to have_received(:add).with(:email, :blocked).once - end - end - - context 'when e-mail provider is not blocked' do - let(:blocked_email) { false } - - it 'does not add errors' do - subject - - expect(errors).to_not have_received(:add) - end - - context 'when canonical e-mail is blocked' do - let(:other_user) { Fabricate(:user, email: 'i.n.f.o@mail.com') } - - before do - other_user.account.suspend! - end - - it 'adds error' do - subject - - expect(errors).to have_received(:add).with(:email, :taken).once - end - end - end - end -end diff --git a/spec/validators/disallowed_hashtags_validator_spec.rb b/spec/validators/disallowed_hashtags_validator_spec.rb deleted file mode 100644 index 570ddb31c2..0000000000 --- a/spec/validators/disallowed_hashtags_validator_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe DisallowedHashtagsValidator do - let(:disallowed_tags) { [] } - - describe '#validate' do - before do - disallowed_tags.each { |name| Fabricate(:tag, name: name, usable: false) } - described_class.new.validate(status) - end - - let(:status) { instance_double(Status, errors: errors, local?: local, reblog?: reblog, text: disallowed_tags.map { |x| "##{x}" }.join(' ')) } - let(:errors) { instance_double(ActiveModel::Errors, add: nil) } - - context 'with a remote reblog' do - let(:local) { false } - let(:reblog) { true } - - it 'does not add errors' do - expect(errors).to_not have_received(:add).with(:text, any_args) - end - end - - context 'with a local original status' do - let(:local) { true } - let(:reblog) { false } - - context 'when does not contain any disallowed hashtags' do - let(:disallowed_tags) { [] } - - it 'does not add errors' do - expect(errors).to_not have_received(:add).with(:text, any_args) - end - end - - context 'when contains disallowed hashtags' do - let(:disallowed_tags) { %w(a b c) } - - it 'adds an error' do - expect(errors).to have_received(:add) - .with(:text, I18n.t('statuses.disallowed_hashtags', tags: disallowed_tags.join(', '), count: disallowed_tags.size)) - end - end - end - end -end diff --git a/spec/validators/email_mx_validator_spec.rb b/spec/validators/email_mx_validator_spec.rb deleted file mode 100644 index bc26be8729..0000000000 --- a/spec/validators/email_mx_validator_spec.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe EmailMxValidator do - describe '#validate' do - let(:user) { instance_double(User, email: 'foo@example.com', sign_up_ip: '1.2.3.4', errors: instance_double(ActiveModel::Errors, add: nil)) } - let(:resolv_dns_double) { instance_double(Resolv::DNS) } - - context 'with an e-mail domain that is explicitly allowed' do - around do |block| - tmp = Rails.configuration.x.email_domains_whitelist - Rails.configuration.x.email_domains_whitelist = 'example.com' - block.call - Rails.configuration.x.email_domains_whitelist = tmp - end - - it 'does not add errors if there are no DNS records' do - configure_resolver('example.com') - - subject.validate(user) - expect(user.errors).to_not have_received(:add) - end - end - - it 'adds no error if there are DNS records for the e-mail domain' do - configure_resolver('example.com', a: resolv_double_a('192.0.2.42')) - - subject.validate(user) - 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 = instance_double(User, email: 'foo@example.com', errors: instance_double(ActiveModel::Errors, 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 = instance_double(User, email: 'foo@', errors: instance_double(ActiveModel::Errors, 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 - configure_resolver('example..com', a: resolv_double_a('192.0.2.42')) - - user = instance_double(User, email: 'foo@example..com', sign_up_ip: '1.2.3.4', errors: instance_double(ActiveModel::Errors, add: nil)) - subject.validate(user) - expect(user.errors).to have_received(:add) - end - - it 'adds an error if there are no DNS records for the e-mail domain' do - configure_resolver('example.com') - - subject.validate(user) - expect(user.errors).to have_received(:add) - end - - it 'adds an error if a MX record does not lead to an IP' do - configure_resolver('example.com', mx: resolv_double_mx('mail.example.com')) - configure_resolver('mail.example.com') - - subject.validate(user) - expect(user.errors).to have_received(:add) - end - - it 'adds an error if the MX record is blacklisted' do - EmailDomainBlock.create!(domain: 'mail.example.com') - - configure_resolver( - 'example.com', - mx: resolv_double_mx('mail.example.com') - ) - configure_resolver( - 'mail.example.com', - a: instance_double(Resolv::DNS::Resource::IN::A, address: '2.3.4.5'), - aaaa: instance_double(Resolv::DNS::Resource::IN::AAAA, address: 'fd00::2') - ) - - subject.validate(user) - expect(user.errors).to have_received(:add) - end - end - - def configure_resolver(domain, options = {}) - allow(resolv_dns_double) - .to receive(:getresources) - .with(domain, Resolv::DNS::Resource::IN::MX) - .and_return(Array(options[:mx])) - allow(resolv_dns_double) - .to receive(:getresources) - .with(domain, Resolv::DNS::Resource::IN::A) - .and_return(Array(options[:a])) - allow(resolv_dns_double) - .to receive(:getresources) - .with(domain, Resolv::DNS::Resource::IN::AAAA) - .and_return(Array(options[:aaaa])) - allow(resolv_dns_double) - .to receive(:timeouts=) - .and_return(nil) - allow(Resolv::DNS) - .to receive(:open) - .and_yield(resolv_dns_double) - end - - def resolv_double_mx(domain) - instance_double(Resolv::DNS::Resource::MX, exchange: domain) - end - - def resolv_double_a(domain) - Resolv::DNS::Resource::IN::A.new(domain) - end -end diff --git a/spec/validators/existing_username_validator_spec.rb b/spec/validators/existing_username_validator_spec.rb deleted file mode 100644 index 4f1dd55a17..0000000000 --- a/spec/validators/existing_username_validator_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe ExistingUsernameValidator do - let(:record_class) do - Class.new do - include ActiveModel::Validations - attr_accessor :contact, :friends - - def self.name - 'Record' - end - - validates :contact, existing_username: true - validates :friends, existing_username: { multiple: true } - end - end - let(:record) { record_class.new } - - describe '#validate_each' do - context 'with a nil value' do - it 'does not add errors' do - record.contact = nil - - expect(record).to be_valid - expect(record.errors).to be_empty - end - end - - context 'when there are no accounts' do - it 'adds errors to the record' do - record.contact = 'user@example.com' - - expect(record).to_not be_valid - expect(record.errors.first.attribute).to eq(:contact) - expect(record.errors.first.type).to eq I18n.t('existing_username_validator.not_found') - end - end - - context 'when there are accounts' do - before { Fabricate(:account, domain: 'example.com', username: 'user') } - - context 'when the value does not match' do - it 'adds errors to the record' do - record.contact = 'friend@other.host' - - expect(record).to_not be_valid - expect(record.errors.first.attribute).to eq(:contact) - expect(record.errors.first.type).to eq I18n.t('existing_username_validator.not_found') - end - - context 'when multiple is true' do - it 'adds errors to the record' do - record.friends = 'friend@other.host' - - expect(record).to_not be_valid - expect(record.errors.first.attribute).to eq(:friends) - expect(record.errors.first.type).to eq I18n.t('existing_username_validator.not_found_multiple', usernames: 'friend@other.host') - end - end - end - - context 'when the value does match' do - it 'does not add errors to the record' do - record.contact = 'user@example.com' - - expect(record).to be_valid - expect(record.errors).to be_empty - end - - context 'when multiple is true' do - it 'does not add errors to the record' do - record.friends = 'user@example.com' - - expect(record).to be_valid - expect(record.errors).to be_empty - end - end - end - end - end -end diff --git a/spec/validators/follow_limit_validator_spec.rb b/spec/validators/follow_limit_validator_spec.rb deleted file mode 100644 index e069b0ed3a..0000000000 --- a/spec/validators/follow_limit_validator_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe FollowLimitValidator do - describe '#validate' do - context 'with a nil account' do - it 'does not add validation errors to base' do - follow = Fabricate.build(:follow, account: nil) - - follow.valid? - - expect(follow.errors[:base]).to be_empty - end - end - - context 'with a non-local account' do - it 'does not add validation errors to base' do - follow = Fabricate.build(:follow, account: Account.new(domain: 'host.example')) - - follow.valid? - - expect(follow.errors[:base]).to be_empty - end - end - - context 'with a local account' do - let(:account) { Account.new } - - context 'when the followers count is under the limit' do - before do - allow(account).to receive(:following_count).and_return(described_class::LIMIT - 100) - end - - it 'does not add validation errors to base' do - follow = Fabricate.build(:follow, account: account) - - follow.valid? - - expect(follow.errors[:base]).to be_empty - end - end - - context 'when the following count is over the limit' do - before do - allow(account).to receive(:following_count).and_return(described_class::LIMIT + 100) - end - - context 'when the followers count is low' do - before do - allow(account).to receive(:followers_count).and_return(10) - end - - it 'adds validation errors to base' do - follow = Fabricate.build(:follow, account: account) - - follow.valid? - - expect(follow.errors[:base]).to include(I18n.t('users.follow_limit_reached', limit: described_class::LIMIT)) - end - end - - context 'when the followers count is high' do - before do - allow(account).to receive(:followers_count).and_return(100_000) - end - - it 'does not add validation errors to base' do - follow = Fabricate.build(:follow, account: account) - - follow.valid? - - expect(follow.errors[:base]).to be_empty - end - end - end - end - end -end diff --git a/spec/validators/language_validator_spec.rb b/spec/validators/language_validator_spec.rb deleted file mode 100644 index cb693dcd81..0000000000 --- a/spec/validators/language_validator_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe LanguageValidator do - let(:record_class) do - Class.new do - include ActiveModel::Validations - attr_accessor :locale - - validates :locale, language: true - end - end - let(:record) { record_class.new } - - describe '#validate_each' do - context 'with a nil value' do - it 'does not add errors' do - record.locale = nil - - expect(record).to be_valid - expect(record.errors).to be_empty - end - end - - context 'with an array of values' do - it 'does not add errors with array of existing locales' do - record.locale = %w(en fr) - - expect(record).to be_valid - expect(record.errors).to be_empty - end - - it 'adds errors with array having some non-existing locales' do - record.locale = %w(en fr missing) - - expect(record).to_not be_valid - expect(record.errors.first.attribute).to eq(:locale) - expect(record.errors.first.type).to eq(:invalid) - end - end - - context 'with a locale string' do - it 'does not add errors when string is an existing locale' do - record.locale = 'en' - - expect(record).to be_valid - expect(record.errors).to be_empty - end - - it 'adds errors when string is non-existing locale' do - record.locale = 'missing' - - expect(record).to_not be_valid - expect(record.errors.first.attribute).to eq(:locale) - expect(record.errors.first.type).to eq(:invalid) - end - end - end -end diff --git a/spec/validators/note_length_validator_spec.rb b/spec/validators/note_length_validator_spec.rb deleted file mode 100644 index 3bca93a283..0000000000 --- a/spec/validators/note_length_validator_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe NoteLengthValidator do - subject { described_class.new(attributes: { note: true }, maximum: 500) } - - describe '#validate' do - it 'adds an error when text is over configured character limit' do - text = 'a' * 520 - account = instance_double(Account, note: text, errors: activemodel_errors) - - subject.validate_each(account, 'note', text) - expect(account.errors).to have_received(:add) - end - - it 'reduces calculated length of auto-linkable space-separated URLs' do - text = [starting_string, example_link].join(' ') - account = instance_double(Account, note: text, errors: activemodel_errors) - - subject.validate_each(account, 'note', text) - expect(account.errors).to_not have_received(:add) - end - - it 'does not reduce calculated length of non-autolinkable URLs' do - text = [starting_string, example_link].join - account = instance_double(Account, note: text, errors: activemodel_errors) - - subject.validate_each(account, 'note', text) - expect(account.errors).to have_received(:add) - end - - private - - def starting_string - 'a' * 476 - end - - def example_link - "http://#{'b' * 30}.com/example" - end - - def activemodel_errors - instance_double(ActiveModel::Errors, add: nil) - end - end -end diff --git a/spec/validators/poll_validator_spec.rb b/spec/validators/poll_validator_spec.rb deleted file mode 100644 index f2a2534898..0000000000 --- a/spec/validators/poll_validator_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe PollValidator do - describe '#validate' do - before do - validator.validate(poll) - end - - let(:validator) { described_class.new } - let(:poll) { instance_double(Poll, options: options, expires_at: expires_at, errors: errors) } - let(:errors) { instance_double(ActiveModel::Errors, add: nil) } - let(:options) { %w(foo bar) } - let(:expires_at) { 1.day.from_now } - - it 'have no errors' do - expect(errors).to_not have_received(:add) - end - - context 'when expires is just 5 min ago' do - let(:expires_at) { 5.minutes.from_now } - - it 'not calls errors add' do - expect(errors).to_not have_received(:add) - end - end - end -end diff --git a/spec/validators/reaction_validator_spec.rb b/spec/validators/reaction_validator_spec.rb deleted file mode 100644 index f99c1cb5f9..0000000000 --- a/spec/validators/reaction_validator_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe ReactionValidator do - let(:announcement) { Fabricate(:announcement) } - - describe '#validate' do - it 'adds error when not a valid unicode emoji' do - reaction = announcement.announcement_reactions.build(name: 'F') - subject.validate(reaction) - expect(reaction.errors).to_not be_empty - end - - it 'does not add error when non-unicode emoji is a custom emoji' do - custom_emoji = Fabricate(:custom_emoji) - reaction = announcement.announcement_reactions.build(name: custom_emoji.shortcode, custom_emoji_id: custom_emoji.id) - subject.validate(reaction) - expect(reaction.errors).to be_empty - end - - it 'adds error when reaction limit count has already been reached' do - stub_const 'ReactionValidator::LIMIT', 2 - %w(🐘 ❤️).each do |name| - announcement.announcement_reactions.create!(name: name, account: Fabricate(:account)) - end - - reaction = announcement.announcement_reactions.build(name: '😘') - subject.validate(reaction) - expect(reaction.errors).to_not be_empty - end - - it 'does not add error when new reaction is part of the existing ones' do - %w(🐘 ❤️ 🙉 😍 😋 😂 😞 👍).each do |name| - announcement.announcement_reactions.create!(name: name, account: Fabricate(:account)) - end - - reaction = announcement.announcement_reactions.build(name: '😋') - subject.validate(reaction) - expect(reaction.errors).to be_empty - end - end -end diff --git a/spec/validators/status_length_validator_spec.rb b/spec/validators/status_length_validator_spec.rb deleted file mode 100644 index 249b90f490..0000000000 --- a/spec/validators/status_length_validator_spec.rb +++ /dev/null @@ -1,109 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe StatusLengthValidator do - describe '#validate' do - before { stub_const("#{described_class}::MAX_CHARS", 500) } # Example values below are relative to this baseline - - it 'does not add errors onto remote statuses' do - status = instance_double(Status, local?: false) - allow(status).to receive(:errors) - - subject.validate(status) - - expect(status).to_not have_received(:errors) - end - - it 'does not add errors onto local reblogs' do - status = instance_double(Status, local?: false, reblog?: true) - allow(status).to receive(:errors) - - subject.validate(status) - - expect(status).to_not have_received(:errors) - end - - it 'adds an error when content warning is over character limit' do - status = status_double(spoiler_text: 'a' * 520) - subject.validate(status) - expect(status.errors).to have_received(:add) - end - - it 'adds an error when text is over character limit' do - status = status_double(text: 'a' * 520) - subject.validate(status) - expect(status.errors).to have_received(:add) - end - - it 'adds an error when text and content warning are over character limit total' do - status = status_double(spoiler_text: 'a' * 250, text: 'b' * 251) - subject.validate(status) - expect(status.errors).to have_received(:add) - end - - it 'reduces calculated length of auto-linkable space-separated URLs' do - text = [starting_string, example_link].join(' ') - status = status_double(text: text) - - subject.validate(status) - expect(status.errors).to_not have_received(:add) - end - - it 'does not reduce calculated length of non-autolinkable URLs' do - text = [starting_string, example_link].join - status = status_double(text: text) - - subject.validate(status) - expect(status.errors).to have_received(:add) - end - - it 'does not reduce calculated length of count overly long URLs' do - text = "http://example.com/valid?#{'#foo?' * 1000}" - status = status_double(text: text) - subject.validate(status) - expect(status.errors).to have_received(:add) - end - - it 'counts only the front part of remote usernames' do - text = ('a' * 475) + " @alice@#{'b' * 30}.com" - status = status_double(text: text) - - subject.validate(status) - expect(status.errors).to_not have_received(:add) - end - - it 'does count both parts of remote usernames for overly long domains' do - text = "@alice@#{'b' * 500}.com" - status = status_double(text: text) - - subject.validate(status) - expect(status.errors).to have_received(:add) - end - end - - private - - def starting_string - 'a' * 476 - end - - def example_link - "http://#{'b' * 30}.com/example" - end - - def status_double(spoiler_text: '', text: '') - instance_double( - Status, - spoiler_text: spoiler_text, - text: text, - errors: activemodel_errors, - local?: true, - reblog?: false - ) - end - - def activemodel_errors - instance_double(ActiveModel::Errors, add: nil) - end -end diff --git a/spec/validators/status_pin_validator_spec.rb b/spec/validators/status_pin_validator_spec.rb deleted file mode 100644 index e50a952db8..0000000000 --- a/spec/validators/status_pin_validator_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe StatusPinValidator do - describe '#validate' do - before do - subject.validate(pin) - end - - let(:pin) { instance_double(StatusPin, account: account, errors: errors, status: status, account_id: pin_account_id) } - let(:status) { instance_double(Status, reblog?: reblog, account_id: status_account_id, visibility: visibility, direct_visibility?: visibility == 'direct') } - let(:account) { instance_double(Account, status_pins: status_pins, local?: local) } - let(:status_pins) { instance_double(Array, count: count) } - let(:errors) { instance_double(ActiveModel::Errors, add: nil) } - let(:pin_account_id) { 1 } - let(:status_account_id) { 1 } - let(:visibility) { 'public' } - let(:local) { false } - let(:reblog) { false } - let(:count) { 0 } - - context 'when pin.status.reblog?' do - let(:reblog) { true } - - it 'calls errors.add' do - expect(errors).to have_received(:add).with(:base, I18n.t('statuses.pin_errors.reblog')) - end - end - - context 'when pin.account_id != pin.status.account_id' do - let(:pin_account_id) { 1 } - let(:status_account_id) { 2 } - - it 'calls errors.add' do - expect(errors).to have_received(:add).with(:base, I18n.t('statuses.pin_errors.ownership')) - end - end - - context 'when pin.status.direct_visibility?' do - let(:visibility) { 'direct' } - - it 'calls errors.add' do - expect(errors).to have_received(:add).with(:base, I18n.t('statuses.pin_errors.direct')) - end - end - - context 'when pin account is local and has too many pins' do - let(:count) { described_class::PIN_LIMIT + 1 } - let(:local) { true } - - it 'calls errors.add' do - expect(errors).to have_received(:add).with(:base, I18n.t('statuses.pin_errors.limit')) - end - end - end -end diff --git a/spec/validators/unique_username_validator_spec.rb b/spec/validators/unique_username_validator_spec.rb deleted file mode 100644 index 0d172c8408..0000000000 --- a/spec/validators/unique_username_validator_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe UniqueUsernameValidator do - describe '#validate' do - context 'when local account' do - it 'does not add errors if username is nil' do - account = instance_double(Account, username: nil, domain: nil, persisted?: false, errors: activemodel_errors) - subject.validate(account) - expect(account.errors).to_not have_received(:add) - end - - it 'does not add errors when existing one is subject itself' do - account = Fabricate(:account, username: 'abcdef') - expect(account).to be_valid - end - - it 'adds an error when the username is already used with ignoring cases' do - Fabricate(:account, username: 'ABCdef') - account = instance_double(Account, username: 'abcDEF', domain: nil, persisted?: false, errors: activemodel_errors) - subject.validate(account) - expect(account.errors).to have_received(:add) - end - - it 'does not add errors when same username remote account exists' do - Fabricate(:account, username: 'abcdef', domain: 'example.com') - account = instance_double(Account, username: 'abcdef', domain: nil, persisted?: false, errors: activemodel_errors) - subject.validate(account) - expect(account.errors).to_not have_received(:add) - end - end - end - - context 'when remote account' do - it 'does not add errors if username is nil' do - account = instance_double(Account, username: nil, domain: 'example.com', persisted?: false, errors: activemodel_errors) - subject.validate(account) - expect(account.errors).to_not have_received(:add) - end - - it 'does not add errors when existing one is subject itself' do - account = Fabricate(:account, username: 'abcdef', domain: 'example.com') - expect(account).to be_valid - end - - it 'adds an error when the username is already used with ignoring cases' do - Fabricate(:account, username: 'ABCdef', domain: 'example.com') - account = instance_double(Account, username: 'abcDEF', domain: 'example.com', persisted?: false, errors: activemodel_errors) - subject.validate(account) - expect(account.errors).to have_received(:add) - end - - it 'adds an error when the domain is already used with ignoring cases' do - Fabricate(:account, username: 'ABCdef', domain: 'example.com') - account = instance_double(Account, username: 'ABCdef', domain: 'EXAMPLE.COM', persisted?: false, errors: activemodel_errors) - subject.validate(account) - expect(account.errors).to have_received(:add) - end - - it 'does not add errors when account with the same username and another domain exists' do - Fabricate(:account, username: 'abcdef', domain: 'example.com') - account = instance_double(Account, username: 'abcdef', domain: 'example2.com', persisted?: false, errors: activemodel_errors) - subject.validate(account) - expect(account.errors).to_not have_received(:add) - end - end - - private - - def activemodel_errors - instance_double(ActiveModel::Errors, add: nil) - end -end diff --git a/spec/validators/unreserved_username_validator_spec.rb b/spec/validators/unreserved_username_validator_spec.rb deleted file mode 100644 index 0eb5f83683..0000000000 --- a/spec/validators/unreserved_username_validator_spec.rb +++ /dev/null @@ -1,121 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe UnreservedUsernameValidator do - let(:record_class) do - Class.new do - include ActiveModel::Validations - attr_accessor :username - - validates_with UnreservedUsernameValidator - end - end - let(:record) { record_class.new } - - describe '#validate' do - context 'when username is nil' do - it 'does not add errors' do - record.username = nil - - expect(record).to be_valid - expect(record.errors).to be_empty - end - end - - context 'when PAM is enabled' do - before do - allow(Devise).to receive(:pam_authentication).and_return(true) - end - - context 'with a pam service available' do - let(:service) { double } - let(:pam_class) do - Class.new do - def self.account(service, username); end - end - end - - before do - stub_const('Rpam2', pam_class) - allow(Devise).to receive(:pam_controlled_service).and_return(service) - end - - context 'when the account exists' do - before do - allow(Rpam2).to receive(:account).with(service, 'username').and_return(true) - end - - it 'adds errors to the record' do - record.username = 'username' - - expect(record).to_not be_valid - expect(record.errors.first.attribute).to eq(:username) - expect(record.errors.first.type).to eq(:reserved) - end - end - - context 'when the account does not exist' do - before do - allow(Rpam2).to receive(:account).with(service, 'username').and_return(false) - end - - it 'does not add errors to the record' do - record.username = 'username' - - expect(record).to be_valid - expect(record.errors).to be_empty - end - end - end - - context 'without a pam service' do - before do - allow(Devise).to receive(:pam_controlled_service).and_return(false) - end - - context 'when there are not any reserved usernames' do - before do - stub_reserved_usernames(nil) - end - - it 'does not add errors to the record' do - record.username = 'username' - - expect(record).to be_valid - expect(record.errors).to be_empty - end - end - - context 'when there are reserved usernames' do - before do - stub_reserved_usernames(%w(alice bob)) - end - - context 'when the username is reserved' do - it 'adds errors to the record' do - record.username = 'alice' - - expect(record).to_not be_valid - expect(record.errors.first.attribute).to eq(:username) - expect(record.errors.first.type).to eq(:reserved) - end - end - - context 'when the username is not reserved' do - it 'does not add errors to the record' do - record.username = 'chris' - - expect(record).to be_valid - expect(record.errors).to be_empty - end - end - end - - def stub_reserved_usernames(value) - allow(Setting).to receive(:[]).with('reserved_usernames').and_return(value) - end - end - end - end -end diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb deleted file mode 100644 index 4f32b7b399..0000000000 --- a/spec/validators/url_validator_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe URLValidator do - let(:record_class) do - Class.new do - include ActiveModel::Validations - attr_accessor :profile - - validates :profile, url: true - end - end - let(:record) { record_class.new } - - describe '#validate_each' do - context 'with a nil value' do - it 'adds errors' do - record.profile = nil - - expect(record).to_not be_valid - expect(record.errors.first.attribute).to eq(:profile) - expect(record.errors.first.type).to eq(:invalid) - end - end - - context 'with an invalid url scheme' do - it 'adds errors' do - record.profile = 'ftp://example.com/page' - - expect(record).to_not be_valid - expect(record.errors.first.attribute).to eq(:profile) - expect(record.errors.first.type).to eq(:invalid) - end - end - - context 'without a hostname' do - it 'adds errors' do - record.profile = 'https:///page' - - expect(record).to_not be_valid - expect(record.errors.first.attribute).to eq(:profile) - expect(record.errors.first.type).to eq(:invalid) - end - end - - context 'with an unparseable value' do - it 'adds errors' do - record.profile = 'https://host:port/page' # non-numeric port string causes invalid uri error - - expect(record).to_not be_valid - expect(record.errors.first.attribute).to eq(:profile) - expect(record.errors.first.type).to eq(:invalid) - end - end - - context 'with a valid url' do - it 'does not add errors' do - record.profile = 'https://example.com/page' - - expect(record).to be_valid - expect(record.errors).to be_empty - end - end - end -end diff --git a/spec/views/admin/trends/links/_preview_card.html.haml_spec.rb b/spec/views/admin/trends/links/_preview_card.html.haml_spec.rb deleted file mode 100644 index 82a1dee6d7..0000000000 --- a/spec/views/admin/trends/links/_preview_card.html.haml_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'admin/trends/links/_preview_card.html.haml' do - it 'correctly escapes user supplied url values' do - form = instance_double(ActionView::Helpers::FormHelper, check_box: nil) - trend = PreviewCardTrend.new(allowed: false) - preview_card = Fabricate.build( - :preview_card, - url: 'https://host.example/path?query=