Merge remote-tracking branch 'parent/main' into upstream-20231109

This commit is contained in:
KMY 2023-11-09 12:33:50 +09:00
commit fc1b280d59
65 changed files with 18502 additions and 13905 deletions

View file

@ -4,7 +4,7 @@ FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye
# Install Rails # Install Rails
# RUN gem install rails webdrivers # RUN gem install rails webdrivers
ARG NODE_VERSION="16" ARG NODE_VERSION="20"
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
# [Optional] Uncomment this section to install additional OS packages. # [Optional] Uncomment this section to install additional OS packages.

View file

@ -11,7 +11,7 @@ bundle install
git checkout -- Gemfile.lock git checkout -- Gemfile.lock
# Fetch Javascript dependencies # Fetch Javascript dependencies
yarn --frozen-lockfile yarn install --immutable
# [re]create, migrate, and seed the test database # [re]create, migrate, and seed the test database
RAILS_ENV=test ./bin/rails db:setup RAILS_ENV=test ./bin/rails db:setup

View file

@ -11,9 +11,32 @@ runs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
cache: yarn
node-version-file: '.nvmrc' 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@v3
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 - name: Install all yarn packages
shell: bash shell: bash
run: yarn --frozen-lockfile ${{ inputs.onlyProduction != 'false' && '--production' || '' }} 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'

9
.gitignore vendored
View file

@ -55,6 +55,15 @@ npm-debug.log
yarn-error.log yarn-error.log
yarn-debug.log yarn-debug.log
# From https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Ignore vagrant log files # Ignore vagrant log files
*-cloudimg-console.log *-cloudimg-console.log

View file

@ -71,26 +71,6 @@ RSpec/AnyInstance:
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 22 Max: 22
# Configuration parameters: AssignmentOnly.
RSpec/InstanceVariable:
Exclude:
- 'spec/controllers/api/v1/streaming_controller_spec.rb'
- 'spec/controllers/auth/confirmations_controller_spec.rb'
- 'spec/controllers/auth/passwords_controller_spec.rb'
- 'spec/controllers/auth/sessions_controller_spec.rb'
- 'spec/controllers/concerns/export_controller_concern_spec.rb'
- 'spec/controllers/home_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb'
- 'spec/controllers/statuses_cleanup_controller_spec.rb'
- 'spec/models/concerns/account_finder_concern_spec.rb'
- 'spec/models/concerns/account_interactions_spec.rb'
- 'spec/models/public_feed_spec.rb'
- 'spec/serializers/activitypub/note_serializer_spec.rb'
- 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
- 'spec/services/remove_status_service_spec.rb'
- 'spec/services/search_service_spec.rb'
- 'spec/services/unblock_domain_service_spec.rb'
RSpec/LetSetup: RSpec/LetSetup:
Exclude: Exclude:
- 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb' - 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb'
@ -558,14 +538,6 @@ Style/SingleArgumentDig:
Exclude: Exclude:
- 'lib/webpacker/manifest_extensions.rb' - 'lib/webpacker/manifest_extensions.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: require_parentheses, require_no_parentheses
Style/StabbyLambdaParentheses:
Exclude:
- 'config/environments/production.rb'
- 'config/initializers/content_security_policy.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
Style/StderrPuts: Style/StderrPuts:
Exclude: Exclude:
@ -625,5 +597,3 @@ Style/TrailingCommaInHashLiteral:
Style/WordArray: Style/WordArray:
Exclude: Exclude:
- 'app/helpers/languages_helper.rb' - 'app/helpers/languages_helper.rb'
- 'spec/controllers/settings/imports_controller_spec.rb'
- 'spec/models/form/import_spec.rb'

0
.yarn/.gitkeep Normal file
View file

View file

@ -0,0 +1,13 @@
diff --git a/lib/index.js b/lib/index.js
index 16ed6be8be8f555cc99096c2ff60954b42dc313d..d009c069770d066ad0db7ad02de1ea473a29334e 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -99,7 +99,7 @@ function lodash(_ref) {
var node = _ref3;
- if ((0, _types.isModuleDeclaration)(node)) {
+ if ((0, _types.isImportDeclaration)(node) || (0, _types.isExportDeclaration)(node)) {
isModule = true;
break;
}

View file

@ -0,0 +1,22 @@
diff --git a/dist/index.js b/dist/index.js
index 57e375592d984e9a429bcd9f800fa2d15cd662e4..0c47d96df3608e23adfd77d887a8f72abbd501c0 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
});
exports.default = void 0;
-var _crypto = _interopRequireDefault(require("crypto"));
+var _createHash = _interopRequireDefault(require("webpack/lib/util/createHash"));
var _path = _interopRequireDefault(require("path"));
@@ -227,7 +227,7 @@ class CompressionPlugin {
originalAlgorithm: this.options.algorithm,
compressionOptions: this.options.compressionOptions,
name,
- contentHash: _crypto.default.createHash("md4").update(input).digest("hex")
+ contentHash: _createHash.default("md4").update(input).digest("hex")
};
} else {
cacheData.name = (0, _serializeJavascript.default)({

View file

@ -1,49 +0,0 @@
# test directories
__tests__
test
tests
powered-test
# asset directories
docs
doc
website
images
# assets
# examples
example
examples
# code coverage directories
coverage
.nyc_output
# build scripts
Makefile
Gulpfile.js
Gruntfile.js
# configs
.tern-project
.gitattributes
.editorconfig
.*ignore
.eslintrc
.jshintrc
.flowconfig
.documentup.json
.yarn-metadata.json
.*.yml
*.yml
# misc
*.gz
*.md
# for specific ignore
!.svgo.yml
!sass-lint/**/*.yml
# breaks lint-staged or generally anything using https://github.com/eemeli/yaml/issues/384
!**/yaml/dist/**/doc

1
.yarnrc.yml Normal file
View file

@ -0,0 +1 @@
nodeLinker: node-modules

View file

@ -13,7 +13,6 @@ ENV DEBIAN_FRONTEND="noninteractive" \
SHELL ["/bin/bash", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]
WORKDIR /opt/mastodon WORKDIR /opt/mastodon
COPY Gemfile* package.json yarn.lock /opt/mastodon/
# hadolint ignore=DL3008 # hadolint ignore=DL3008
RUN apt-get update && \ RUN apt-get update && \
@ -36,8 +35,14 @@ RUN apt-get update && \
bundle config set --local deployment 'true' && \ bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \ bundle config set --local without 'development test' && \
bundle config set silence_root_warning true && \ bundle config set silence_root_warning true && \
bundle install -j"$(nproc)" && \ corepack enable
yarn install --pure-lockfile --production --network-timeout 600000 && \
COPY Gemfile* package.json yarn.lock .yarnrc.yml /opt/mastodon/
COPY .yarn /opt/mastodon/.yarn
RUN bundle install -j"$(nproc)"
RUN yarn workspaces focus --all --production && \
yarn cache clean yarn cache clean
FROM node:${NODE_VERSION} FROM node:${NODE_VERSION}
@ -78,7 +83,8 @@ RUN apt-get update && \
tzdata \ tzdata \
libreadline8 \ libreadline8 \
tini && \ tini && \
ln -s /opt/mastodon /mastodon ln -s /opt/mastodon /mastodon && \
corepack enable
# Note: no, cleaning here since Debian does this automatically # Note: no, cleaning here since Debian does this automatically
# See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem # See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem

4
Vagrantfile vendored
View file

@ -112,11 +112,11 @@ bundle install
# Install node modules # Install node modules
sudo corepack enable sudo corepack enable
yarn set version classic corepack prepare
yarn install yarn install
# Build Mastodon # Build Mastodon
export RAILS_ENV=development export RAILS_ENV=development
export $(cat ".env.vagrant" | xargs) export $(cat ".env.vagrant" | xargs)
bundle exec rails db:setup bundle exec rails db:setup

View file

@ -86,6 +86,11 @@ module Admin
private private
def batched_ordered_status_edits
@status.edits.reorder(nil).includes(:account, status: [:account]).find_each(order: :asc)
end
helper_method :batched_ordered_status_edits
def admin_status_batch_action_params def admin_status_batch_action_params
params.require(:admin_status_batch_action).permit(status_ids: []) params.require(:admin_status_batch_action).permit(status_ids: [])
end end

View file

@ -298,5 +298,3 @@ module LanguagesHelper
locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym) locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym)
end end
end end
# rubocop:enable Metrics/ModuleLength

View file

@ -0,0 +1,80 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { render, fireEvent } from '@testing-library/react';
class Media extends Component {
constructor(props) {
super(props);
this.state = {
paused: props.paused || false,
};
}
handleMediaClick = () => {
const { onClick } = this.props;
this.setState(prevState => ({
paused: !prevState.paused,
}));
if (typeof onClick === 'function') {
onClick();
}
const { title } = this.props;
const mediaElements = document.querySelectorAll(`div[title="${title}"]`);
setTimeout(() => {
mediaElements.forEach(element => {
if (element !== this && !element.classList.contains('paused')) {
element.click();
}
});
}, 0);
};
render() {
const { title } = this.props;
const { paused } = this.state;
return (
<button title={title} onClick={this.handleMediaClick}>
Media Component - {paused ? 'Paused' : 'Playing'}
</button>
);
}
}
Media.propTypes = {
title: PropTypes.string.isRequired,
onClick: PropTypes.func,
paused: PropTypes.bool,
};
describe('Media attachments test', () => {
let currentMedia = null;
const togglePlayMock = jest.fn();
it('plays a new media file and pauses others that were playing', () => {
const container = render(
<div>
<Media title='firstMedia' paused onClick={togglePlayMock} />
<Media title='secondMedia' paused onClick={togglePlayMock} />
</div>,
);
fireEvent.click(container.getByTitle('firstMedia'));
expect(togglePlayMock).toHaveBeenCalledTimes(1);
currentMedia = container.getByTitle('firstMedia');
expect(currentMedia.textContent).toMatch(/Playing/);
fireEvent.click(container.getByTitle('secondMedia'));
expect(togglePlayMock).toHaveBeenCalledTimes(2);
currentMedia = container.getByTitle('secondMedia');
expect(currentMedia.textContent).toMatch(/Playing/);
});
});

View file

@ -20,6 +20,7 @@ import { formatTime, getPointerPosition, fileNameFromURL } from 'mastodon/featur
import { Blurhash } from '../../components/blurhash'; import { Blurhash } from '../../components/blurhash';
import { displayMedia, useBlurhash } from '../../initial_state'; import { displayMedia, useBlurhash } from '../../initial_state';
import { currentMedia, setCurrentMedia } from '../../reducers/media_attachments';
import Visualizer from './visualizer'; import Visualizer from './visualizer';
@ -165,15 +166,32 @@ class Audio extends PureComponent {
} }
togglePlay = () => { togglePlay = () => {
if (!this.audioContext) { const audios = document.querySelectorAll('audio');
this._initAudioContext();
audios.forEach((audio) => {
const button = audio.previousElementSibling;
button.addEventListener('click', () => {
if(audio.paused) {
audios.forEach((e) => {
if (e !== audio) {
e.pause();
}
});
audio.play();
this.setState({ paused: false });
} else {
audio.pause();
this.setState({ paused: true });
}
});
});
if (currentMedia !== null) {
currentMedia.pause();
} }
if (this.state.paused) { this.audio.play();
this.setState({ paused: false }, () => this.audio.play()); setCurrentMedia(this.audio);
} else {
this.setState({ paused: true }, () => this.audio.pause());
}
}; };
handleResize = debounce(() => { handleResize = debounce(() => {
@ -195,6 +213,7 @@ class Audio extends PureComponent {
}; };
handlePause = () => { handlePause = () => {
this.audio.pause();
this.setState({ paused: true }); this.setState({ paused: true });
if (this.audioContext) { if (this.audioContext) {

View file

@ -13,7 +13,7 @@ class CircleSelect extends PureComponent {
static propTypes = { static propTypes = {
unavailable: PropTypes.bool, unavailable: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
circles: ImmutablePropTypes.list, circles: ImmutablePropTypes.map,
circleId: PropTypes.string, circleId: PropTypes.string,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
}; };

View file

@ -95,7 +95,7 @@ const makeMapStateToProps = () => {
const getPictureInPicture = makeGetPictureInPicture(); const getPictureInPicture = makeGetPictureInPicture();
const getReferenceIds = createSelector([ const getReferenceIds = createSelector([
(state, { id }) => state.getIn(['contexts', 'references', id]), (state, { id }) => state.getIn(['contexts', 'references', id]) || Immutable.List(),
], (references) => { ], (references) => {
return references; return references;
}); });

View file

@ -27,7 +27,7 @@ const Account = connect(state => ({
const messages = defineMessages({ const messages = defineMessages({
search: { id: 'navigation_bar.search', defaultMessage: 'Search' }, search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
reload: { id: 'navigation_bar.reload', defaultMessage: 'Reload' }, reload: { id: 'navigation_bar.refresh', defaultMessage: 'Refresh' },
}); });
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({

View file

@ -22,6 +22,7 @@ import { Icon } from 'mastodon/components/icon';
import { playerSettings } from 'mastodon/settings'; import { playerSettings } from 'mastodon/settings';
import { displayMedia, useBlurhash } from '../../initial_state'; import { displayMedia, useBlurhash } from '../../initial_state';
import { currentMedia, setCurrentMedia } from '../../reducers/media_attachments';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
const messages = defineMessages({ const messages = defineMessages({
@ -181,6 +182,7 @@ class Video extends PureComponent {
}; };
handlePause = () => { handlePause = () => {
this.video.pause();
this.setState({ paused: true }); this.setState({ paused: true });
}; };
@ -344,11 +346,32 @@ class Video extends PureComponent {
}; };
togglePlay = () => { togglePlay = () => {
if (this.state.paused) { const videos = document.querySelectorAll('video');
this.setState({ paused: false }, () => this.video.play());
} else { videos.forEach((video) => {
this.setState({ paused: true }, () => this.video.pause()); const button = video.nextElementSibling;
button.addEventListener('click', () => {
if (video.paused) {
videos.forEach((e) => {
if (e !== video) {
e.pause();
}
});
video.play();
this.setState({ paused: false });
} else {
video.pause();
this.setState({ paused: true });
}
});
});
if (currentMedia !== null) {
currentMedia.pause();
} }
this.video.play();
setCurrentMedia(this.video);
}; };
toggleFullscreen = () => { toggleFullscreen = () => {

View file

@ -88,7 +88,7 @@
"attachments_list.unprocessed": "(ausstehend)", "attachments_list.unprocessed": "(ausstehend)",
"audio.hide": "Audio ausblenden", "audio.hide": "Audio ausblenden",
"autosuggest_hashtag.per_week": "{count} pro Woche", "autosuggest_hashtag.per_week": "{count} pro Woche",
"boost_modal.combo": "Drücke {combo}, um das beim nächsten Mal zu überspringen", "boost_modal.combo": "Mit {combo} wird dieses Fenster beim nächsten Mal nicht mehr angezeigt",
"bundle_column_error.copy_stacktrace": "Fehlerbericht kopieren", "bundle_column_error.copy_stacktrace": "Fehlerbericht kopieren",
"bundle_column_error.error.body": "Die angeforderte Seite konnte nicht dargestellt werden. Dies könnte auf einen Fehler in unserem Code oder auf ein Browser-Kompatibilitätsproblem zurückzuführen sein.", "bundle_column_error.error.body": "Die angeforderte Seite konnte nicht dargestellt werden. Dies könnte auf einen Fehler in unserem Code oder auf ein Browser-Kompatibilitätsproblem zurückzuführen sein.",
"bundle_column_error.error.title": "Oh nein!", "bundle_column_error.error.title": "Oh nein!",

View file

@ -691,6 +691,7 @@
"status.media.show": "Click to show", "status.media.show": "Click to show",
"status.media_hidden": "Media hidden", "status.media_hidden": "Media hidden",
"status.mention": "Mention @{name}", "status.mention": "Mention @{name}",
"status.mentions": "Mentioned accounts",
"status.more": "More", "status.more": "More",
"status.mute": "Mute @{name}", "status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",

View file

@ -62,7 +62,7 @@
"account.share": "שתף את הפרופיל של @{name}", "account.share": "שתף את הפרופיל של @{name}",
"account.show_reblogs": "הצג הדהודים מאת @{name}", "account.show_reblogs": "הצג הדהודים מאת @{name}",
"account.statuses_counter": "{count, plural, one {הודעה} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}", "account.statuses_counter": "{count, plural, one {הודעה} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}",
"account.unblock": "הסר את החסימה של @{name}", "account.unblock": "להסיר חסימה ל- @{name}",
"account.unblock_domain": "הסירי את החסימה של קהילת {domain}", "account.unblock_domain": "הסירי את החסימה של קהילת {domain}",
"account.unblock_short": "הסר חסימה", "account.unblock_short": "הסר חסימה",
"account.unendorse": "אל תקדם בפרופיל", "account.unendorse": "אל תקדם בפרופיל",

View file

@ -777,6 +777,7 @@
"status.media.show": "クリックして表示", "status.media.show": "クリックして表示",
"status.media_hidden": "非表示のメディア", "status.media_hidden": "非表示のメディア",
"status.mention": "@{name}さんに投稿", "status.mention": "@{name}さんに投稿",
"status.mentions": "メンション先一覧",
"status.more": "もっと見る", "status.more": "もっと見る",
"status.mute": "@{name}さんをミュート", "status.mute": "@{name}さんをミュート",
"status.mute_conversation": "会話をミュート", "status.mute_conversation": "会話をミュート",

View file

@ -222,7 +222,7 @@
"emoji_button.search_results": "Výsledky hľadania", "emoji_button.search_results": "Výsledky hľadania",
"emoji_button.symbols": "Symboly", "emoji_button.symbols": "Symboly",
"emoji_button.travel": "Cestovanie a miesta", "emoji_button.travel": "Cestovanie a miesta",
"empty_column.account_suspended": "Účet bol vylúčený", "empty_column.account_suspended": "Účet bol pozastavený",
"empty_column.account_timeline": "Nie sú tu žiadne príspevky!", "empty_column.account_timeline": "Nie sú tu žiadne príspevky!",
"empty_column.account_unavailable": "Profil nedostupný", "empty_column.account_unavailable": "Profil nedostupný",
"empty_column.blocks": "Ešte si nikoho nezablokoval/a.", "empty_column.blocks": "Ešte si nikoho nezablokoval/a.",

View file

@ -2,6 +2,13 @@ import { Map as ImmutableMap } from 'immutable';
import { STORE_HYDRATE } from '../actions/store'; import { STORE_HYDRATE } from '../actions/store';
export let currentMedia = null;
export function setCurrentMedia(value) {
currentMedia = value;
}
const initialState = ImmutableMap({ const initialState = ImmutableMap({
accept_content_types: [], accept_content_types: [],
}); });

View file

@ -60,7 +60,7 @@
%h3= t('admin.statuses.history') %h3= t('admin.statuses.history')
%ol.history %ol.history
- @status.edits.reorder(nil).includes(:account, status: [:account]).find_each(order: :asc).with_index do |status_edit, i| - batched_ordered_status_edits.with_index do |status_edit, i|
%li %li
.history__entry .history__entry
%h5 %h5

View file

@ -1,6 +1,6 @@
default: &default default: &default
adapter: postgresql adapter: postgresql
pool: <%= ENV["DB_POOL"] || ENV['MAX_THREADS'] || 5 %> pool: <%= ENV["DB_POOL"] || (if Sidekiq.server? then Sidekiq[:concurrency] else ENV['MAX_THREADS'] end) || 5 %>
timeout: 5000 timeout: 5000
connect_timeout: 15 connect_timeout: 15
encoding: unicode encoding: unicode

View file

@ -44,7 +44,7 @@ Rails.application.configure do
config.force_ssl = true config.force_ssl = true
config.ssl_options = { config.ssl_options = {
redirect: { redirect: {
exclude: ->request { request.path.start_with?('/health') || request.headers["Host"].end_with?('.onion') || request.headers["Host"].end_with?('.i2p') } exclude: ->(request) { request.path.start_with?('/health') || request.headers["Host"].end_with?('.onion') || request.headers["Host"].end_with?('.i2p') }
} }
} }

View file

@ -67,7 +67,7 @@ end
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# Rails.application.config.content_security_policy_report_only = true # Rails.application.config.content_security_policy_report_only = true
Rails.application.config.content_security_policy_nonce_generator = ->request { SecureRandom.base64(16) } Rails.application.config.content_security_policy_nonce_generator = ->(request) { SecureRandom.base64(16) }
Rails.application.config.content_security_policy_nonce_directives = %w(style-src) Rails.application.config.content_security_policy_nonce_directives = %w(style-src)

View file

@ -556,6 +556,7 @@ be:
total_reported: Скаргі на іх total_reported: Скаргі на іх
total_storage: Медыя дадаткі total_storage: Медыя дадаткі
totals_time_period_hint_html: Паказаныя агульныя значэнні ніжэй уключаюць даныя за ўвесь час. totals_time_period_hint_html: Паказаныя агульныя значэнні ніжэй уключаюць даныя за ўвесь час.
unknown_instance: На дадзены момант няма запісаў аб гэтым дамене на гэтым серверы.
invites: invites:
deactivate_all: Дэактываваць усё deactivate_all: Дэактываваць усё
filter: filter:
@ -1076,6 +1077,14 @@ be:
hint_html: Засталася яшчэ адна рэч! Каб не дапусціць спаму, нам трэба пацвердзіць, што вы чалавек. Разгадайце CAPTCHA ніжэй і націсніце «Працягнуць». hint_html: Засталася яшчэ адна рэч! Каб не дапусціць спаму, нам трэба пацвердзіць, што вы чалавек. Разгадайце CAPTCHA ніжэй і націсніце «Працягнуць».
title: Праверка бяспекі title: Праверка бяспекі
confirmations: confirmations:
awaiting_review: Ваш электронны адрас пацверджаны! Адміністрацыя %{domain} зараз разглядае вашу рэгістрацыю. Вы атрымаеце паведамленне па электроннай пошце, калі ваш уліковы запіс будзе ўхвалены!
awaiting_review_title: Ваша рэгістрацыя разглядаецца
clicking_this_link: націснуць на гэту спасылку
login_link: увайсці
proceed_to_login_html: Цяпер вы можаце перайсці да %{login_link}.
redirect_to_app_html: Вы павінны былі быць перанакіраваны ў праграму <strong>%{app_name}</strong>. Калі гэтага не адбылося, паспрабуйце %{clicking_this_link} або вярніцеся да праграмы ўручную.
registration_complete: Ваша рэгістрацыя на %{domain} завершана!
welcome_title: Вітаем, %{name}!
wrong_email_hint: Калі гэты адрас электроннай пошты памылковы, вы можаце змяніць яго ў наладах уліковага запісу. wrong_email_hint: Калі гэты адрас электроннай пошты памылковы, вы можаце змяніць яго ў наладах уліковага запісу.
delete_account: Выдаліць уліковы запіс delete_account: Выдаліць уліковы запіс
delete_account_html: Калі вы жадаеце выдаліць ваш уліковы запіс, можаце <a href="%{path}">працягнуць тут</a>. Ад вас будзе запатрабавана пацвярджэнне. delete_account_html: Калі вы жадаеце выдаліць ваш уліковы запіс, можаце <a href="%{path}">працягнуць тут</a>. Ад вас будзе запатрабавана пацвярджэнне.
@ -1137,6 +1146,7 @@ be:
functional: Ваш уліковы запіс поўнасцю працуе. functional: Ваш уліковы запіс поўнасцю працуе.
pending: Ваша заяўка разглядаецца нашым супрацоўнікам. Гэта можа заняць некаторы час. Вы атрымаеце электронны ліст, калі заяўка будзе ўхвалена. pending: Ваша заяўка разглядаецца нашым супрацоўнікам. Гэта можа заняць некаторы час. Вы атрымаеце электронны ліст, калі заяўка будзе ўхвалена.
redirecting_to: Ваш уліковы запіс неактыўны, бо ў цяперашні час ён перанакіроўваецца на %{acct}. redirecting_to: Ваш уліковы запіс неактыўны, бо ў цяперашні час ён перанакіроўваецца на %{acct}.
self_destruct: Паколькі %{domain} зачыняецца, вы атрымаеце толькі абмежаваны доступ да свайго уліковага запісу.
view_strikes: Праглядзець мінулыя папярэджанні для вашага ўліковага запісу view_strikes: Праглядзець мінулыя папярэджанні для вашага ўліковага запісу
too_fast: Форма адпраўлена занадта хутка, паспрабуйце яшчэ раз. too_fast: Форма адпраўлена занадта хутка, паспрабуйце яшчэ раз.
use_security_key: Выкарыстаеце ключ бяспекі use_security_key: Выкарыстаеце ключ бяспекі
@ -1622,6 +1632,9 @@ be:
over_daily_limit: Вы перавысілі ліміт ў %{limit} запланаваных на сёння допісаў over_daily_limit: Вы перавысілі ліміт ў %{limit} запланаваных на сёння допісаў
over_total_limit: Вы перавысілі ліміт ў %{limit} запланаваных допісаў over_total_limit: Вы перавысілі ліміт ў %{limit} запланаваных допісаў
too_soon: Запланаваная дата мусіць быць у будучыні too_soon: Запланаваная дата мусіць быць у будучыні
self_destruct:
lead_html: На жаль, дамен <strong>%{domain}</strong> зачыняецца назаўсёды. Калі ў вас быў уліковы запіс, вы не зможаце працягваць выкарыстоўваць яго, але вы ўсё яшчэ можаце запытаць рэзервовае капіраванне вашых даных.
title: Гэты сервер зачыняецца
sessions: sessions:
activity: Апошняя актыўнасць activity: Апошняя актыўнасць
browser: Браўзер browser: Браўзер

View file

@ -1601,7 +1601,7 @@ ko:
windows_mobile: 윈도우 모바일 windows_mobile: 윈도우 모바일
windows_phone: 윈도우 폰 windows_phone: 윈도우 폰
revoke: 삭제 revoke: 삭제
revoke_success: 세션을 성공적으로 취소하였습니다 revoke_success: 세션을 성공적으로 삭제하였습니다
title: 세션 title: 세션
view_authentication_history: 내 계정에 대한 인증 이력 보기 view_authentication_history: 내 계정에 대한 인증 이력 보기
settings: settings:

View file

@ -98,7 +98,7 @@ sk:
disabled: Blokovaný disabled: Blokovaný
pending: Čakajúci pending: Čakajúci
silenced: Obmedzený silenced: Obmedzený
suspended: Vylúčený/á suspended: Pozastavený/á
title: Moderácia title: Moderácia
moderation_notes: Moderátorské poznámky moderation_notes: Moderátorské poznámky
most_recent_activity: Posledná aktivita most_recent_activity: Posledná aktivita
@ -149,8 +149,8 @@ sk:
statuses: Príspevkov statuses: Príspevkov
strikes: Predchádzajúce údery strikes: Predchádzajúce údery
subscribe: Odoberaj subscribe: Odoberaj
suspend: Vylúč suspend: Pozastav
suspended: Vylúčený/á suspended: Pozastavený/á
suspension_irreversible: Údaje tohto účtu boli nenávratne vymazané. Účet môžete zrušiť, aby sa dal používať, ale neobnovia sa žiadne údaje, ktoré predtým mal. suspension_irreversible: Údaje tohto účtu boli nenávratne vymazané. Účet môžete zrušiť, aby sa dal používať, ale neobnovia sa žiadne údaje, ktoré predtým mal.
suspension_reversible_hint_html: Účet bol pozastavený a údaje budú úplne odstránené dňa %{date}. Dovtedy je možné účet obnoviť bez akýchkoľvek nepriaznivých účinkov. Ak chcete okamžite odstrániť všetky údaje účtu, môžete tak urobiť nižšie. suspension_reversible_hint_html: Účet bol pozastavený a údaje budú úplne odstránené dňa %{date}. Dovtedy je možné účet obnoviť bez akýchkoľvek nepriaznivých účinkov. Ak chcete okamžite odstrániť všetky údaje účtu, môžete tak urobiť nižšie.
title: Účty title: Účty
@ -162,6 +162,7 @@ sk:
undo_suspension: Zruš blokovanie undo_suspension: Zruš blokovanie
unsilenced_msg: Úspešne zrušené obmedzenie účtu %{username} unsilenced_msg: Úspešne zrušené obmedzenie účtu %{username}
unsubscribe: Prestaň odoberať unsubscribe: Prestaň odoberať
unsuspended_msg: "%{username} ov/in účet úspešne spojazdnený"
username: Prezývka username: Prezývka
view_domain: Ukáž súhrn pre doménu view_domain: Ukáž súhrn pre doménu
warn: Varuj warn: Varuj
@ -209,7 +210,7 @@ sk:
resolve_report: Vyrieš nahlásený problém resolve_report: Vyrieš nahlásený problém
sensitive_account: Vynúť všetky médiá na účte ako chúlostivé sensitive_account: Vynúť všetky médiá na účte ako chúlostivé
silence_account: Utíš účet silence_account: Utíš účet
suspend_account: Vylúč účet suspend_account: Pozastav účet
unassigned_report: Odober priradenie nahlásenia unassigned_report: Odober priradenie nahlásenia
unblock_email_account: Odblokuj emailovú adresu unblock_email_account: Odblokuj emailovú adresu
unsilence_account: Zvráť obmedzenie účtu unsilence_account: Zvráť obmedzenie účtu
@ -255,6 +256,7 @@ sk:
silence_account_html: "%{name} obmedzil/a účet %{target}" silence_account_html: "%{name} obmedzil/a účet %{target}"
suspend_account_html: "%{name} zablokoval/a účet používateľa %{target}" suspend_account_html: "%{name} zablokoval/a účet používateľa %{target}"
unassigned_report_html: "%{name} odobral/a report od %{target}" unassigned_report_html: "%{name} odobral/a report od %{target}"
unsuspend_account_html: "%{name} spojazdnil/a účet %{target}"
update_user_role_html: "%{name} zmenil/a rolu pre %{target}" update_user_role_html: "%{name} zmenil/a rolu pre %{target}"
deleted_account: zmazaný účet deleted_account: zmazaný účet
empty: Žiadne záznamy nenájdené. empty: Žiadne záznamy nenájdené.
@ -341,6 +343,7 @@ sk:
confirm_suspension: confirm_suspension:
cancel: Zruš cancel: Zruš
confirm: Vylúč confirm: Vylúč
preamble_html: Chystáš sa vylúčiť <strong>%{domain}</strong> a jej poddomény.
title: Potvrď blokovanie domény %{domain} title: Potvrď blokovanie domény %{domain}
created_msg: Doména je v štádiu blokovania created_msg: Doména je v štádiu blokovania
destroyed_msg: Blokovanie domény bolo zrušené destroyed_msg: Blokovanie domény bolo zrušené
@ -355,7 +358,7 @@ sk:
severity: severity:
noop: Nič noop: Nič
silence: Obmedz silence: Obmedz
suspend: Vylúč suspend: Pozastav
title: Nové blokovanie domény title: Nové blokovanie domény
not_permitted: Nemáš povolenie na vykonanie tohto kroku not_permitted: Nemáš povolenie na vykonanie tohto kroku
obfuscate: Zatemniť názov domény obfuscate: Zatemniť názov domény
@ -416,7 +419,7 @@ sk:
reject_media: Zamietni médiá reject_media: Zamietni médiá
reject_reports: Zamietni hlásenia reject_reports: Zamietni hlásenia
silence: Obmedzená silence: Obmedzená
suspend: Vylúč suspend: Pozastav
policy: Zásady policy: Zásady
reason: Verejné odôvodnenie reason: Verejné odôvodnenie
title: Zásady o obsahu title: Zásady o obsahu
@ -537,7 +540,7 @@ sk:
statuses: Nahlásený obsah statuses: Nahlásený obsah
summary: summary:
action_preambles: action_preambles:
suspend_html: 'Chystáš sa <strong>vylúčiť</strong> účet <strong>@%{acct}</strong>. To urobí:' suspend_html: 'Chystáš sa <strong>pozastaviť</strong> účet <strong>@%{acct}</strong>. To urobí:'
actions: actions:
delete_html: Vymaž pohoršujúce príspevky delete_html: Vymaž pohoršujúce príspevky
mark_as_sensitive_html: Označ médiá pohoršujúcich príspevkov za chúlostivé mark_as_sensitive_html: Označ médiá pohoršujúcich príspevkov za chúlostivé

View file

@ -3,6 +3,18 @@
require 'sidekiq_unique_jobs/web' require 'sidekiq_unique_jobs/web'
require 'sidekiq-scheduler/web' require 'sidekiq-scheduler/web'
class RedirectWithVary < ActionDispatch::Routing::PathRedirect
def build_response(req)
super.tap do |response|
response.headers['Vary'] = 'Origin, Accept'
end
end
end
def redirect_with_vary(path)
RedirectWithVary.new(301, path)
end
Rails.application.routes.draw do Rails.application.routes.draw do
# Paths of routes on the web app that to not require to be indexed or # Paths of routes on the web app that to not require to be indexed or
# have alternative format representations requiring separate controllers # have alternative format representations requiring separate controllers
@ -97,10 +109,13 @@ Rails.application.routes.draw do
confirmations: 'auth/confirmations', confirmations: 'auth/confirmations',
} }
get '/users/:username', to: redirect('/@%{username}'), constraints: lambda { |req| req.format.nil? || req.format.html? } # rubocop:disable Style/FormatStringToken - those do not go through the usual formatting functions and are not safe to correct
get '/users/:username/following', to: redirect('/@%{username}/following'), constraints: lambda { |req| req.format.nil? || req.format.html? } get '/users/:username', to: redirect_with_vary('/@%{username}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
get '/users/:username/followers', to: redirect('/@%{username}/followers'), constraints: lambda { |req| req.format.nil? || req.format.html? } get '/users/:username/following', to: redirect_with_vary('/@%{username}/following'), constraints: lambda { |req| req.format.nil? || req.format.html? }
get '/users/:username/statuses/:id', to: redirect('/@%{username}/%{id}'), constraints: lambda { |req| req.format.nil? || req.format.html? } get '/users/:username/followers', to: redirect_with_vary('/@%{username}/followers'), constraints: lambda { |req| req.format.nil? || req.format.html? }
get '/users/:username/statuses/:id', to: redirect_with_vary('/@%{username}/%{id}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
# rubocop:enable Style/FormatStringToken
get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" } get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" }
resources :accounts, path: 'users', only: [:show], param: :username do resources :accounts, path: 'users', only: [:show], param: :username do

View file

@ -4,7 +4,7 @@ const { createHash } = require('crypto');
const { readFileSync } = require('fs'); const { readFileSync } = require('fs');
const { resolve } = require('path'); const { resolve } = require('path');
const CompressionPlugin = require('@renchap/compression-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const { merge } = require('webpack-merge'); const { merge } = require('webpack-merge');

View file

@ -16,6 +16,8 @@ const config = {
'!app/javascript/mastodon/service_worker/entry.js', '!app/javascript/mastodon/service_worker/entry.js',
'!app/javascript/mastodon/test_setup.js', '!app/javascript/mastodon/test_setup.js',
], ],
// Those packages are ESM, so we need them to be processed by Babel
transformIgnorePatterns: ['/node_modules/(?!(redent|strip-indent)/)'],
coverageDirectory: '<rootDir>/coverage', coverageDirectory: '<rootDir>/coverage',
moduleDirectories: ['<rootDir>/node_modules', '<rootDir>/app/javascript'], moduleDirectories: ['<rootDir>/node_modules', '<rootDir>/app/javascript'],
moduleNameMapper: { moduleNameMapper: {

34
lib/tasks/webpacker.rake Normal file
View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
# Disable this task as we use pnpm
require 'semantic_range'
Rake::Task['webpacker:check_yarn'].clear
namespace :webpacker do
desc 'Verifies if Yarn is installed'
task check_yarn: :environment do
begin
yarn_version = `yarn --version`.strip
raise Errno::ENOENT if yarn_version.blank?
yarn_range = '>=4 <5'
is_valid = begin
SemanticRange.satisfies?(yarn_version, yarn_range)
rescue
false
end
unless is_valid
warn "Mastodon and Webpacker requires Yarn \"#{yarn_range}\" and you are using #{yarn_version}"
warn 'Exiting!'
exit!
end
rescue Errno::ENOENT
warn 'Yarn not installed. Please see the Mastodon documentation to install the correct version.'
warn 'Exiting!'
exit!
end
end
end

View file

@ -2,8 +2,11 @@
"name": "@mastodon/mastodon", "name": "@mastodon/mastodon",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"engines": { "engines": {
"node": ">=16" "node": ">=18"
}, },
"workspaces": [
"."
],
"scripts": { "scripts": {
"build:development": "cross-env RAILS_ENV=development NODE_ENV=development ./bin/webpack", "build:development": "cross-env RAILS_ENV=development NODE_ENV=development ./bin/webpack",
"build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack", "build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack",
@ -52,7 +55,6 @@
"@material-symbols/svg-600": "^0.13.1", "@material-symbols/svg-600": "^0.13.1",
"@rails/ujs": "^7.1.1", "@rails/ujs": "^7.1.1",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"@renchap/compression-webpack-plugin": "^6.1.4",
"@svgr/webpack": "^5.5.0", "@svgr/webpack": "^5.5.0",
"arrow-key-navigation": "^1.2.0", "arrow-key-navigation": "^1.2.0",
"async-mutex": "^0.4.0", "async-mutex": "^0.4.0",
@ -60,7 +62,7 @@
"axios": "^1.4.0", "axios": "^1.4.0",
"babel-loader": "^8.3.0", "babel-loader": "^8.3.0",
"babel-plugin-formatjs": "^10.5.1", "babel-plugin-formatjs": "^10.5.1",
"babel-plugin-lodash": "^3.3.4", "babel-plugin-lodash": "patch:babel-plugin-lodash@npm%3A3.3.4#~/.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch",
"babel-plugin-preval": "^5.1.0", "babel-plugin-preval": "^5.1.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24", "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"blurhash": "^2.0.5", "blurhash": "^2.0.5",
@ -68,6 +70,7 @@
"classnames": "^2.3.2", "classnames": "^2.3.2",
"cocoon-js-vanilla": "^1.3.0", "cocoon-js-vanilla": "^1.3.0",
"color-blend": "^4.0.0", "color-blend": "^4.0.0",
"compression-webpack-plugin": "patch:compression-webpack-plugin@npm%3A6.1.1#~/.yarn/patches/compression-webpack-plugin-npm-6.1.1-3a2a65987e.patch",
"core-js": "^3.30.2", "core-js": "^3.30.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^5.2.7", "css-loader": "^5.2.7",
@ -220,10 +223,20 @@
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"resolutions": { "resolutions": {
"@types/react": "^18.0.26",
"kind-of": "^6.0.3", "kind-of": "^6.0.3",
"webpack/terser-webpack-plugin": "^4.2.3" "webpack/terser-webpack-plugin": "^4.2.3"
}, },
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-router-dom": {
"optional": true
}
},
"optionalDependencies": { "optionalDependencies": {
"bufferutil": "^4.0.7", "bufferutil": "^4.0.7",
"utf-8-validate": "^6.0.3" "utf-8-validate": "^6.0.3"
@ -234,5 +247,6 @@
"*.{haml}": "bundle exec haml-lint", "*.{haml}": "bundle exec haml-lint",
"*.{js,jsx,ts,tsx}": "eslint --fix", "*.{js,jsx,ts,tsx}": "eslint --fix",
"*.{css,scss}": "stylelint --fix" "*.{css,scss}": "stylelint --fix"
} },
"packageManager": "yarn@4.0.1"
} }

View file

@ -26,7 +26,6 @@ describe Api::V1::StreamingController do
context 'with streaming api on different host' do context 'with streaming api on different host' do
before do before do
Rails.configuration.x.streaming_api_base_url = "wss://streaming-#{Rails.configuration.x.web_domain}" Rails.configuration.x.streaming_api_base_url = "wss://streaming-#{Rails.configuration.x.web_domain}"
@streaming_host = URI.parse(Rails.configuration.x.streaming_api_base_url).host
end end
describe 'GET #index' do describe 'GET #index' do
@ -38,7 +37,13 @@ describe Api::V1::StreamingController do
[:scheme, :path, :query, :fragment].each do |part| [:scheme, :path, :query, :fragment].each do |part|
expect(redirect_to_uri.send(part)).to eq(request_uri.send(part)), "redirect target #{part}" expect(redirect_to_uri.send(part)).to eq(request_uri.send(part)), "redirect target #{part}"
end end
expect(redirect_to_uri.host).to eq(@streaming_host), 'redirect target host' expect(redirect_to_uri.host).to eq(streaming_host), 'redirect target host'
end
private
def streaming_host
URI.parse(Rails.configuration.x.streaming_api_base_url).host
end end
end end
end end

View file

@ -7,7 +7,7 @@ describe Auth::ConfirmationsController do
describe 'GET #new' do describe 'GET #new' do
it 'returns http success' do it 'returns http success' do
@request.env['devise.mapping'] = Devise.mappings[:user] request.env['devise.mapping'] = Devise.mappings[:user]
get :new get :new
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
@ -19,7 +19,7 @@ describe Auth::ConfirmationsController do
before do before do
allow(BootstrapTimelineWorker).to receive(:perform_async) allow(BootstrapTimelineWorker).to receive(:perform_async)
@request.env['devise.mapping'] = Devise.mappings[:user] request.env['devise.mapping'] = Devise.mappings[:user]
get :show, params: { confirmation_token: 'foobar' } get :show, params: { confirmation_token: 'foobar' }
end end
@ -37,7 +37,7 @@ describe Auth::ConfirmationsController do
before do before do
allow(BootstrapTimelineWorker).to receive(:perform_async) allow(BootstrapTimelineWorker).to receive(:perform_async)
@request.env['devise.mapping'] = Devise.mappings[:user] request.env['devise.mapping'] = Devise.mappings[:user]
get :show, params: { confirmation_token: 'foobar' } get :show, params: { confirmation_token: 'foobar' }
end end
@ -51,7 +51,7 @@ describe Auth::ConfirmationsController do
before do before do
allow(BootstrapTimelineWorker).to receive(:perform_async) allow(BootstrapTimelineWorker).to receive(:perform_async)
@request.env['devise.mapping'] = Devise.mappings[:user] request.env['devise.mapping'] = Devise.mappings[:user]
sign_in(user, scope: :user) sign_in(user, scope: :user)
get :show, params: { confirmation_token: 'foobar' } get :show, params: { confirmation_token: 'foobar' }
end end
@ -66,7 +66,7 @@ describe Auth::ConfirmationsController do
before do before do
allow(BootstrapTimelineWorker).to receive(:perform_async) allow(BootstrapTimelineWorker).to receive(:perform_async)
@request.env['devise.mapping'] = Devise.mappings[:user] request.env['devise.mapping'] = Devise.mappings[:user]
user.approved = false user.approved = false
user.save! user.save!
sign_in(user, scope: :user) sign_in(user, scope: :user)
@ -83,7 +83,7 @@ describe Auth::ConfirmationsController do
before do before do
allow(BootstrapTimelineWorker).to receive(:perform_async) allow(BootstrapTimelineWorker).to receive(:perform_async)
@request.env['devise.mapping'] = Devise.mappings[:user] request.env['devise.mapping'] = Devise.mappings[:user]
get :show, params: { confirmation_token: 'foobar' } get :show, params: { confirmation_token: 'foobar' }
end end

View file

@ -7,7 +7,7 @@ describe Auth::PasswordsController do
describe 'GET #new' do describe 'GET #new' do
it 'returns http success' do it 'returns http success' do
@request.env['devise.mapping'] = Devise.mappings[:user] request.env['devise.mapping'] = Devise.mappings[:user]
get :new get :new
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
@ -18,12 +18,14 @@ describe Auth::PasswordsController do
before do before do
request.env['devise.mapping'] = Devise.mappings[:user] request.env['devise.mapping'] = Devise.mappings[:user]
@token = user.send_reset_password_instructions
end end
context 'with valid reset_password_token' do context 'with valid reset_password_token' do
it 'returns http success' do it 'returns http success' do
get :edit, params: { reset_password_token: @token } token = user.send_reset_password_instructions
get :edit, params: { reset_password_token: token }
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
end end
@ -38,9 +40,9 @@ describe Auth::PasswordsController do
describe 'POST #update' do describe 'POST #update' do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:password) { 'reset0password' }
before do before do
@password = 'reset0password'
request.env['devise.mapping'] = Devise.mappings[:user] request.env['devise.mapping'] = Devise.mappings[:user]
end end
@ -50,9 +52,9 @@ describe Auth::PasswordsController do
let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) } let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) }
before do before do
@token = user.send_reset_password_instructions token = user.send_reset_password_instructions
post :update, params: { user: { password: @password, password_confirmation: @password, reset_password_token: @token } } post :update, params: { user: { password: password, password_confirmation: password, reset_password_token: token } }
end end
it 'redirect to sign in' do it 'redirect to sign in' do
@ -63,7 +65,7 @@ describe Auth::PasswordsController do
this_user = User.find(user.id) this_user = User.find(user.id)
expect(this_user).to_not be_nil expect(this_user).to_not be_nil
expect(this_user.valid_password?(@password)).to be true expect(this_user.valid_password?(password)).to be true
end end
it 'deactivates all sessions' do it 'deactivates all sessions' do
@ -81,7 +83,7 @@ describe Auth::PasswordsController do
context 'with invalid reset_password_token' do context 'with invalid reset_password_token' do
before do before do
post :update, params: { user: { password: @password, password_confirmation: @password, reset_password_token: 'some_invalid_value' } } post :update, params: { user: { password: password, password_confirmation: password, reset_password_token: 'some_invalid_value' } }
end end
it 'renders reset password' do it 'renders reset password' do

View file

@ -378,7 +378,7 @@ RSpec.describe Auth::SessionsController do
context 'when using a valid webauthn credential' do context 'when using a valid webauthn credential' do
before do before do
@controller.session[:webauthn_challenge] = challenge controller.session[:webauthn_challenge] = challenge
post :create, params: { user: { credential: fake_credential } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } post :create, params: { user: { credential: fake_credential } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
end end

View file

@ -11,7 +11,7 @@ describe ExportControllerConcern do
end end
def export_data def export_data
@export.account.username 'body data value'
end end
end end
@ -24,7 +24,7 @@ describe ExportControllerConcern do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.media_type).to eq 'text/csv' expect(response.media_type).to eq 'text/csv'
expect(response.headers['Content-Disposition']).to start_with 'attachment; filename="anonymous.csv"' expect(response.headers['Content-Disposition']).to start_with 'attachment; filename="anonymous.csv"'
expect(response.body).to eq user.account.username expect(response.body).to eq 'body data value'
end end
it 'returns unauthorized when not signed in' do it 'returns unauthorized when not signed in' do

View file

@ -10,7 +10,7 @@ RSpec.describe HomeController do
context 'when not signed in' do context 'when not signed in' do
it 'returns http success' do it 'returns http success' do
@request.path = '/' request.path = '/'
expect(subject).to have_http_status(:success) expect(subject).to have_http_status(:success)
end end
end end

View file

@ -194,7 +194,7 @@ RSpec.describe Settings::ImportsController do
let!(:rows) do let!(:rows) do
[ [
{ 'acct' => 'foo@bar' }, { 'acct' => 'foo@bar' },
{ 'acct' => 'user@bar', 'show_reblogs' => false, 'notify' => true, 'languages' => ['fr', 'de'] }, { 'acct' => 'user@bar', 'show_reblogs' => false, 'notify' => true, 'languages' => %w(fr de) },
].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) } ].map { |data| Fabricate(:bulk_import_row, bulk_import: bulk_import, data: data) }
end end

View file

@ -130,7 +130,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
it 'stores the challenge on the session' do it 'stores the challenge on the session' do
get :options get :options
expect(@controller.session[:webauthn_challenge]).to be_present expect(controller.session[:webauthn_challenge]).to be_present
end end
it 'does not change webauthn_id' do it 'does not change webauthn_id' do
@ -155,7 +155,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
it 'stores the challenge on the session' do it 'stores the challenge on the session' do
get :options get :options
expect(@controller.session[:webauthn_challenge]).to be_present expect(controller.session[:webauthn_challenge]).to be_present
end end
it 'sets user webauthn_id' do it 'sets user webauthn_id' do
@ -218,7 +218,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
context 'when creation succeeds' do context 'when creation succeeds' do
it 'returns http success' do it 'returns http success' do
@controller.session[:webauthn_challenge] = challenge controller.session[:webauthn_challenge] = challenge
post :create, params: { credential: new_webauthn_credential, nickname: nickname } post :create, params: { credential: new_webauthn_credential, nickname: nickname }
@ -226,7 +226,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
end end
it 'adds a new credential to user credentials' do it 'adds a new credential to user credentials' do
@controller.session[:webauthn_challenge] = challenge controller.session[:webauthn_challenge] = challenge
expect do expect do
post :create, params: { credential: new_webauthn_credential, nickname: nickname } post :create, params: { credential: new_webauthn_credential, nickname: nickname }
@ -234,7 +234,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
end end
it 'does not change webauthn_id' do it 'does not change webauthn_id' do
@controller.session[:webauthn_challenge] = challenge controller.session[:webauthn_challenge] = challenge
expect do expect do
post :create, params: { credential: new_webauthn_credential, nickname: nickname } post :create, params: { credential: new_webauthn_credential, nickname: nickname }
@ -244,7 +244,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
context 'when the nickname is already used' do context 'when the nickname is already used' do
it 'fails' do it 'fails' do
@controller.session[:webauthn_challenge] = challenge controller.session[:webauthn_challenge] = challenge
post :create, params: { credential: new_webauthn_credential, nickname: 'USB Key' } post :create, params: { credential: new_webauthn_credential, nickname: 'USB Key' }
@ -264,7 +264,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
end end
it 'fails' do it 'fails' do
@controller.session[:webauthn_challenge] = challenge controller.session[:webauthn_challenge] = challenge
post :create, params: { credential: new_webauthn_credential, nickname: nickname } post :create, params: { credential: new_webauthn_credential, nickname: nickname }
@ -277,7 +277,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
context 'when user have not enabled webauthn' do context 'when user have not enabled webauthn' do
context 'when creation succeeds' do context 'when creation succeeds' do
it 'creates a webauthn credential' do it 'creates a webauthn credential' do
@controller.session[:webauthn_challenge] = challenge controller.session[:webauthn_challenge] = challenge
expect do expect do
post :create, params: { credential: new_webauthn_credential, nickname: nickname } post :create, params: { credential: new_webauthn_credential, nickname: nickname }

View file

@ -5,9 +5,10 @@ require 'rails_helper'
RSpec.describe StatusesCleanupController do RSpec.describe StatusesCleanupController do
render_views render_views
let!(:user) { Fabricate(:user) }
before do before do
@user = Fabricate(:user) sign_in user, scope: :user
sign_in @user, scope: :user
end end
describe 'GET #show' do describe 'GET #show' do
@ -30,9 +31,9 @@ RSpec.describe StatusesCleanupController do
end end
it 'updates the account status cleanup policy' do it 'updates the account status cleanup policy' do
expect(@user.account.statuses_cleanup_policy.enabled).to be true expect(user.account.statuses_cleanup_policy.enabled).to be true
expect(@user.account.statuses_cleanup_policy.keep_direct).to be false expect(user.account.statuses_cleanup_policy.keep_direct).to be false
expect(@user.account.statuses_cleanup_policy.keep_polls).to be true expect(user.account.statuses_cleanup_policy.keep_polls).to be true
end end
it 'redirects' do it 'redirects' do

View file

@ -8,12 +8,14 @@ RSpec.describe TranslationService::DeepL do
let(:plan) { 'advanced' } let(:plan) { 'advanced' }
before do before do
stub_request(:get, 'https://api.deepl.com/v2/languages?type=source').to_return( %w(api-free.deepl.com api.deepl.com).each do |host|
body: '[{"language":"EN","name":"English"},{"language":"UK","name":"Ukrainian"}]' stub_request(:get, "https://#{host}/v2/languages?type=source").to_return(
) body: '[{"language":"EN","name":"English"},{"language":"UK","name":"Ukrainian"}]'
stub_request(:get, 'https://api.deepl.com/v2/languages?type=target').to_return( )
body: '[{"language":"EN-GB","name":"English (British)"},{"language":"ZH","name":"Chinese"}]' stub_request(:get, "https://#{host}/v2/languages?type=target").to_return(
) body: '[{"language":"EN-GB","name":"English (British)"},{"language":"ZH","name":"Chinese"}]'
)
end
end end
describe '#translate' do describe '#translate' do
@ -73,28 +75,25 @@ RSpec.describe TranslationService::DeepL do
end end
end end
describe '#request' do describe 'the paid and free plan api hostnames' do
before do before do
stub_request(:any, //) service.languages
# rubocop:disable Lint/EmptyBlock
service.send(:request, :get, '/v2/languages') { |res| }
# rubocop:enable Lint/EmptyBlock
end end
it 'uses paid plan base URL' do context 'without a plan set' do
expect(a_request(:get, 'https://api.deepl.com/v2/languages')).to have_been_made.once it 'uses paid plan base URL and sends an API key' do
end 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
context 'with free plan' do
let(:plan) { 'free' }
it 'uses free plan base URL' do
expect(a_request(:get, 'https://api-free.deepl.com/v2/languages')).to have_been_made.once
end end
end end
it 'sends API key' do context 'with the free plan' do
expect(a_request(:get, 'https://api.deepl.com/v2/languages').with(headers: { Authorization: 'DeepL-Auth-Key my-api-key' })).to have_been_made.once 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 end
end end

View file

@ -4,17 +4,15 @@ require 'rails_helper'
describe AccountFinderConcern do describe AccountFinderConcern do
describe 'local finders' do describe 'local finders' do
before do let!(:account) { Fabricate(:account, username: 'Alice') }
@account = Fabricate(:account, username: 'Alice')
end
describe '.find_local' do describe '.find_local' do
it 'returns case-insensitive result' do it 'returns case-insensitive result' do
expect(Account.find_local('alice')).to eq(@account) expect(Account.find_local('alice')).to eq(account)
end end
it 'returns correctly cased result' do it 'returns correctly cased result' do
expect(Account.find_local('Alice')).to eq(@account) expect(Account.find_local('Alice')).to eq(account)
end end
it 'returns nil without a match' do it 'returns nil without a match' do
@ -36,7 +34,7 @@ describe AccountFinderConcern do
describe '.find_local!' do describe '.find_local!' do
it 'returns matching result' do it 'returns matching result' do
expect(Account.find_local!('alice')).to eq(@account) expect(Account.find_local!('alice')).to eq(account)
end end
it 'raises on non-matching result' do it 'raises on non-matching result' do
@ -54,17 +52,15 @@ describe AccountFinderConcern do
end end
describe 'remote finders' do describe 'remote finders' do
before do let!(:account) { Fabricate(:account, username: 'Alice', domain: 'mastodon.social') }
@account = Fabricate(:account, username: 'Alice', domain: 'mastodon.social')
end
describe '.find_remote' do describe '.find_remote' do
it 'returns exact match result' do it 'returns exact match result' do
expect(Account.find_remote('alice', 'mastodon.social')).to eq(@account) expect(Account.find_remote('alice', 'mastodon.social')).to eq(account)
end end
it 'returns case-insensitive result' do it 'returns case-insensitive result' do
expect(Account.find_remote('ALICE', 'MASTODON.SOCIAL')).to eq(@account) expect(Account.find_remote('ALICE', 'MASTODON.SOCIAL')).to eq(account)
end end
it 'returns nil when username does not match' do it 'returns nil when username does not match' do
@ -90,7 +86,7 @@ describe AccountFinderConcern do
describe '.find_remote!' do describe '.find_remote!' do
it 'returns matching result' do it 'returns matching result' do
expect(Account.find_remote!('alice', 'mastodon.social')).to eq(@account) expect(Account.find_remote!('alice', 'mastodon.social')).to eq(account)
end end
it 'raises on non-matching result' do it 'raises on non-matching result' do

View file

@ -650,38 +650,36 @@ describe AccountInteractions do
end end
describe 'ignoring reblogs from an account' do describe 'ignoring reblogs from an account' do
before do let!(:me) { Fabricate(:account, username: 'Me') }
@me = Fabricate(:account, username: 'Me') let!(:you) { Fabricate(:account, username: 'You') }
@you = Fabricate(:account, username: 'You')
end
context 'with the reblogs option unspecified' do context 'with the reblogs option unspecified' do
before do before do
@me.follow!(@you) me.follow!(you)
end end
it 'defaults to showing reblogs' do it 'defaults to showing reblogs' do
expect(@me.muting_reblogs?(@you)).to be(false) expect(me.muting_reblogs?(you)).to be(false)
end end
end end
context 'with the reblogs option set to false' do context 'with the reblogs option set to false' do
before do before do
@me.follow!(@you, reblogs: false) me.follow!(you, reblogs: false)
end end
it 'does mute reblogs' do it 'does mute reblogs' do
expect(@me.muting_reblogs?(@you)).to be(true) expect(me.muting_reblogs?(you)).to be(true)
end end
end end
context 'with the reblogs option set to true' do context 'with the reblogs option set to true' do
before do before do
@me.follow!(@you, reblogs: true) me.follow!(you, reblogs: true)
end end
it 'does not mute reblogs' do it 'does not mute reblogs' do
expect(@me.muting_reblogs?(@you)).to be(false) expect(me.muting_reblogs?(you)).to be(false)
end end
end end
end end

View file

@ -296,7 +296,7 @@ RSpec.describe Form::Import do
it_behaves_like 'on successful import', 'following', 'merge', 'following_accounts.csv', [ it_behaves_like 'on successful import', 'following', 'merge', 'following_accounts.csv', [
{ 'acct' => 'user@example.com', 'show_reblogs' => true, 'notify' => false, 'languages' => nil }, { 'acct' => 'user@example.com', 'show_reblogs' => true, 'notify' => false, 'languages' => nil },
{ 'acct' => 'user@test.com', 'show_reblogs' => true, 'notify' => true, 'languages' => ['en', 'fr'] }, { 'acct' => 'user@test.com', 'show_reblogs' => true, 'notify' => true, 'languages' => %w(en fr) },
] ]
it_behaves_like 'on successful import', 'muting', 'merge', 'muted_accounts.csv', [ it_behaves_like 'on successful import', 'muting', 'merge', 'muted_accounts.csv', [

View file

@ -209,15 +209,13 @@ RSpec.describe PublicFeed do
end end
describe 'with an account passed in' do describe 'with an account passed in' do
subject { described_class.new(@account).get(20).map(&:id) } subject { described_class.new(account).get(20).map(&:id) }
before do let!(:account) { Fabricate(:account) }
@account = Fabricate(:account)
end
it 'excludes statuses from accounts blocked by the account' do it 'excludes statuses from accounts blocked by the account' do
blocked = Fabricate(:account) blocked = Fabricate(:account)
@account.block!(blocked) account.block!(blocked)
blocked_status = Fabricate(:status, account: blocked) blocked_status = Fabricate(:status, account: blocked)
expect(subject).to_not include(blocked_status.id) expect(subject).to_not include(blocked_status.id)
@ -225,7 +223,7 @@ RSpec.describe PublicFeed do
it 'excludes statuses from accounts who have blocked the account' do it 'excludes statuses from accounts who have blocked the account' do
blocker = Fabricate(:account) blocker = Fabricate(:account)
blocker.block!(@account) blocker.block!(account)
blocked_status = Fabricate(:status, account: blocker) blocked_status = Fabricate(:status, account: blocker)
expect(subject).to_not include(blocked_status.id) expect(subject).to_not include(blocked_status.id)
@ -233,7 +231,7 @@ RSpec.describe PublicFeed do
it 'excludes statuses from accounts muted by the account' do it 'excludes statuses from accounts muted by the account' do
muted = Fabricate(:account) muted = Fabricate(:account)
@account.mute!(muted) account.mute!(muted)
muted_status = Fabricate(:status, account: muted) muted_status = Fabricate(:status, account: muted)
expect(subject).to_not include(muted_status.id) expect(subject).to_not include(muted_status.id)
@ -241,7 +239,7 @@ RSpec.describe PublicFeed do
it 'excludes statuses from accounts from personally blocked domains' do it 'excludes statuses from accounts from personally blocked domains' do
blocked = Fabricate(:account, domain: 'example.com') blocked = Fabricate(:account, domain: 'example.com')
@account.block_domain!(blocked.domain) account.block_domain!(blocked.domain)
blocked_status = Fabricate(:status, account: blocked) blocked_status = Fabricate(:status, account: blocked)
expect(subject).to_not include(blocked_status.id) expect(subject).to_not include(blocked_status.id)
@ -249,7 +247,7 @@ RSpec.describe PublicFeed do
context 'with language preferences' do context 'with language preferences' do
it 'excludes statuses in languages not allowed by the account user' do it 'excludes statuses in languages not allowed by the account user' do
@account.user.update(chosen_languages: [:en, :es]) account.user.update(chosen_languages: [:en, :es])
en_status = Fabricate(:status, language: 'en') en_status = Fabricate(:status, language: 'en')
es_status = Fabricate(:status, language: 'es') es_status = Fabricate(:status, language: 'es')
fr_status = Fabricate(:status, language: 'fr') fr_status = Fabricate(:status, language: 'fr')
@ -260,7 +258,7 @@ RSpec.describe PublicFeed do
end end
it 'includes all languages when user does not have a setting' do it 'includes all languages when user does not have a setting' do
@account.user.update(chosen_languages: nil) account.user.update(chosen_languages: nil)
en_status = Fabricate(:status, language: 'en') en_status = Fabricate(:status, language: 'en')
es_status = Fabricate(:status, language: 'es') es_status = Fabricate(:status, language: 'es')
@ -270,7 +268,7 @@ RSpec.describe PublicFeed do
end end
it 'includes all languages when account does not have a user' do it 'includes all languages when account does not have a user' do
@account.update(user: nil) account.update(user: nil)
en_status = Fabricate(:status, language: 'en') en_status = Fabricate(:status, language: 'en')
es_status = Fabricate(:status, language: 'es') es_status = Fabricate(:status, language: 'es')

View file

@ -11,10 +11,6 @@ if RUN_SYSTEM_SPECS
ENV['STREAMING_API_BASE_URL'] = "http://localhost:#{STREAMING_PORT}" ENV['STREAMING_API_BASE_URL'] = "http://localhost:#{STREAMING_PORT}"
end end
if RUN_SEARCH_SPECS
# Include any configuration or setups specific to search tests here
end
require File.expand_path('../config/environment', __dir__) require File.expand_path('../config/environment', __dir__)
abort('The Rails environment is running in production mode!') if Rails.env.production? abort('The Rails environment is running in production mode!') if Rails.env.production?
@ -35,8 +31,6 @@ Sidekiq.logger = nil
# System tests config # System tests config
DatabaseCleaner.strategy = [:deletion] DatabaseCleaner.strategy = [:deletion]
streaming_server_manager = StreamingServerManager.new
search_data_manager = SearchDataManager.new
Devise::Test::ControllerHelpers.module_eval do Devise::Test::ControllerHelpers.module_eval do
alias_method :original_sign_in, :sign_in alias_method :original_sign_in, :sign_in
@ -100,26 +94,7 @@ RSpec.configure do |config|
Capybara.current_driver = :rack_test Capybara.current_driver = :rack_test
end end
config.before :suite do
if RUN_SYSTEM_SPECS
Webpacker.compile
streaming_server_manager.start(port: STREAMING_PORT)
end
if RUN_SEARCH_SPECS
Chewy.strategy(:urgent)
search_data_manager.prepare_test_data
end
end
config.after :suite do
streaming_server_manager.stop
search_data_manager.cleanup_test_data if RUN_SEARCH_SPECS
end
config.around :each, type: :system do |example| config.around :each, type: :system do |example|
# driven_by :selenium, using: :chrome, screen_size: [1600, 1200]
driven_by :selenium, using: :headless_chrome, screen_size: [1600, 1200] driven_by :selenium, using: :headless_chrome, screen_size: [1600, 1200]
# The streaming server needs access to the database # The streaming server needs access to the database
@ -136,12 +111,6 @@ RSpec.configure do |config|
self.use_transactional_tests = true self.use_transactional_tests = true
end end
config.around :each, type: :search do |example|
search_data_manager.populate_indexes
example.run
search_data_manager.remove_indexes
end
config.before do |example| config.before do |example|
unless example.metadata[:paperclip_processing] unless example.metadata[:paperclip_processing]
allow_any_instance_of(Paperclip::Attachment).to receive(:post_process).and_return(true) # rubocop:disable RSpec/AnyInstance allow_any_instance_of(Paperclip::Attachment).to receive(:post_process).and_return(true) # rubocop:disable RSpec/AnyInstance

View file

@ -119,40 +119,39 @@ module TestEndpoints
end end
describe 'Caching behavior' do describe 'Caching behavior' do
shared_examples 'cachable response' do shared_examples 'cachable response' do |http_success: false|
it 'does not set cookies' do it 'does not set cookies or set public cache control', :aggregate_failures do
expect(response.cookies).to be_empty expect(response.cookies).to be_empty
end
it 'sets public cache control' do
# expect(response.cache_control[:max_age]&.to_i).to be_positive # expect(response.cache_control[:max_age]&.to_i).to be_positive
expect(response.cache_control[:public]).to be_truthy expect(response.cache_control[:public]).to be_truthy
expect(response.cache_control[:private]).to be_falsy expect(response.cache_control[:private]).to be_falsy
expect(response.cache_control[:no_store]).to be_falsy expect(response.cache_control[:no_store]).to be_falsy
expect(response.cache_control[:no_cache]).to be_falsy expect(response.cache_control[:no_cache]).to be_falsy
expect(response).to have_http_status(200) if http_success
end end
end end
shared_examples 'non-cacheable response' do shared_examples 'non-cacheable response' do |http_success: false|
it 'sets private cache control' do it 'sets private cache control' do
expect(response.cache_control[:private]).to be_truthy expect(response.cache_control[:private]).to be_truthy
expect(response.cache_control[:no_store]).to be_truthy expect(response.cache_control[:no_store]).to be_truthy
expect(response).to have_http_status(200) if http_success
end end
end end
shared_examples 'non-cacheable error' do shared_examples 'non-cacheable error' do
it 'does not return HTTP success' 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).to_not have_http_status(200)
end
it 'does not have cache headers' do
expect(response.cache_control[:public]).to be_falsy expect(response.cache_control[:public]).to be_falsy
end end
end end
shared_examples 'language-dependent' do shared_examples 'language-dependent' do
it 'has a Vary on Accept-Language' do it 'has a Vary on Accept-Language' do
expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('accept-language') expect(response_vary_headers).to include('accept-language')
end end
end end
@ -180,6 +179,15 @@ describe 'Caching behavior' do
end end
context 'when anonymously accessed' do 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.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('accept')
end
end
TestEndpoints::ALWAYS_CACHED.each do |endpoint| TestEndpoints::ALWAYS_CACHED.each do |endpoint|
describe endpoint do describe endpoint do
before { get endpoint } before { get endpoint }
@ -196,7 +204,7 @@ describe 'Caching behavior' do
it_behaves_like 'cachable response' it_behaves_like 'cachable response'
it 'has a Vary on Cookie' do it 'has a Vary on Cookie' do
expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('cookie') expect(response_vary_headers).to include('cookie')
end end
it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
@ -210,7 +218,7 @@ describe 'Caching behavior' do
it_behaves_like 'cachable response' it_behaves_like 'cachable response'
it 'has a Vary on Authorization' do it 'has a Vary on Authorization' do
expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('authorization') expect(response_vary_headers).to include('authorization')
end end
it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint) it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
@ -296,7 +304,7 @@ describe 'Caching behavior' do
it_behaves_like 'non-cacheable response' it_behaves_like 'non-cacheable response'
it 'has a Vary on Cookie' do it 'has a Vary on Cookie' do
expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('cookie') expect(response_vary_headers).to include('cookie')
end end
end end
end end
@ -305,11 +313,7 @@ describe 'Caching behavior' do
describe endpoint do describe endpoint do
before { get endpoint } before { get endpoint }
it_behaves_like 'non-cacheable response' it_behaves_like 'non-cacheable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
end end
@ -345,7 +349,7 @@ describe 'Caching behavior' do
it_behaves_like 'non-cacheable response' it_behaves_like 'non-cacheable response'
it 'has a Vary on Authorization' do it 'has a Vary on Authorization' do
expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('authorization') expect(response_vary_headers).to include('authorization')
end end
end end
end end
@ -356,11 +360,7 @@ describe 'Caching behavior' do
get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
end end
it_behaves_like 'non-cacheable response' it_behaves_like 'non-cacheable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
end end
@ -387,11 +387,7 @@ describe 'Caching behavior' do
context 'when allowed for local users only' do context 'when allowed for local users only' do
let(:show_domain_blocks) { 'users' } let(:show_domain_blocks) { 'users' }
it_behaves_like 'non-cacheable response' it_behaves_like 'non-cacheable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
context 'when disabled' do context 'when disabled' do
@ -415,11 +411,7 @@ describe 'Caching behavior' do
get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
end end
it_behaves_like 'cachable response' it_behaves_like 'cachable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
TestEndpoints::REQUIRE_SIGNATURE.each do |endpoint| TestEndpoints::REQUIRE_SIGNATURE.each do |endpoint|
@ -428,11 +420,7 @@ describe 'Caching behavior' do
get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
end end
it_behaves_like 'non-cacheable response' it_behaves_like 'non-cacheable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
end end
end end
@ -450,11 +438,7 @@ describe 'Caching behavior' do
get '/actor', headers: { 'Accept' => 'application/activity+json' } get '/actor', headers: { 'Accept' => 'application/activity+json' }
end end
it_behaves_like 'cachable response' it_behaves_like 'cachable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
(TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
@ -481,11 +465,7 @@ describe 'Caching behavior' do
get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
end end
it_behaves_like 'cachable response' it_behaves_like 'cachable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
(TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
@ -494,11 +474,7 @@ describe 'Caching behavior' do
get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
end end
it_behaves_like 'non-cacheable response' it_behaves_like 'non-cacheable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
end end
end end
@ -522,11 +498,7 @@ describe 'Caching behavior' do
get '/actor', headers: { 'Accept' => 'application/activity+json' } get '/actor', headers: { 'Accept' => 'application/activity+json' }
end end
it_behaves_like 'cachable response' it_behaves_like 'cachable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
(TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
@ -554,11 +526,7 @@ describe 'Caching behavior' do
get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
end end
it_behaves_like 'cachable response' it_behaves_like 'cachable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
(TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
@ -567,11 +535,7 @@ describe 'Caching behavior' do
get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
end end
it_behaves_like 'non-cacheable response' it_behaves_like 'non-cacheable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
end end
end end
@ -585,11 +549,7 @@ describe 'Caching behavior' do
get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' } get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
end end
it_behaves_like 'cachable response' it_behaves_like 'cachable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
(TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint| (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
@ -661,7 +621,7 @@ describe 'Caching behavior' do
it_behaves_like 'non-cacheable response' it_behaves_like 'non-cacheable response'
it 'has a Vary on Authorization' do it 'has a Vary on Authorization' do
expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('authorization') expect(response_vary_headers).to include('authorization')
end end
end end
end end
@ -672,13 +632,15 @@ describe 'Caching behavior' do
get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" } get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
end end
it_behaves_like 'non-cacheable response' it_behaves_like 'non-cacheable response', http_success: true
it 'returns HTTP success' do
expect(response).to have_http_status(200)
end
end end
end end
end end
end end
private
def response_vary_headers
response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }
end
end end

View file

@ -20,71 +20,70 @@ RSpec.describe RemoveStatusService, type: :service do
end end
context 'when removed status is not a reblog' do context 'when removed status is not a reblog' do
let!(:status) { PostStatusService.new.call(alice, text: 'Hello @bob@example.com ThisIsASecret') }
before do before do
@status = PostStatusService.new.call(alice, text: 'Hello @bob@example.com ThisIsASecret') FavouriteService.new.call(jeff, status)
FavouriteService.new.call(jeff, @status) Fabricate(:status, account: bill, reblog: status, uri: 'hoge')
Fabricate(:status, account: bill, reblog: @status, uri: 'hoge')
end end
it 'removes status from author\'s home feed' do it 'removes status from author\'s home feed' do
subject.call(@status) subject.call(status)
expect(HomeFeed.new(alice).get(10).pluck(:id)).to_not include(@status.id) expect(HomeFeed.new(alice).get(10).pluck(:id)).to_not include(status.id)
end end
it 'removes status from local follower\'s home feed' do it 'removes status from local follower\'s home feed' do
subject.call(@status) subject.call(status)
expect(HomeFeed.new(jeff).get(10).pluck(:id)).to_not include(@status.id) expect(HomeFeed.new(jeff).get(10).pluck(:id)).to_not include(status.id)
end end
it 'sends Delete activity to followers' do it 'sends Delete activity to followers' do
subject.call(@status) subject.call(status)
expect(a_request(:post, 'http://example.com/inbox').with( expect(a_request(:post, 'http://example.com/inbox').with(
body: hash_including({ body: hash_including({
'type' => 'Delete', 'type' => 'Delete',
'object' => { 'object' => {
'type' => 'Tombstone', 'type' => 'Tombstone',
'id' => ActivityPub::TagManager.instance.uri_for(@status), 'id' => ActivityPub::TagManager.instance.uri_for(status),
'atomUri' => OStatus::TagManager.instance.uri_for(@status), 'atomUri' => OStatus::TagManager.instance.uri_for(status),
}, },
}) })
)).to have_been_made.once )).to have_been_made.once
end end
it 'sends Delete activity to rebloggers' do it 'sends Delete activity to rebloggers' do
subject.call(@status) subject.call(status)
expect(a_request(:post, 'http://example2.com/inbox').with( expect(a_request(:post, 'http://example2.com/inbox').with(
body: hash_including({ body: hash_including({
'type' => 'Delete', 'type' => 'Delete',
'object' => { 'object' => {
'type' => 'Tombstone', 'type' => 'Tombstone',
'id' => ActivityPub::TagManager.instance.uri_for(@status), 'id' => ActivityPub::TagManager.instance.uri_for(status),
'atomUri' => OStatus::TagManager.instance.uri_for(@status), 'atomUri' => OStatus::TagManager.instance.uri_for(status),
}, },
}) })
)).to have_been_made.once )).to have_been_made.once
end end
it 'remove status from notifications' do it 'remove status from notifications' do
expect { subject.call(@status) }.to change { expect { subject.call(status) }.to change {
Notification.where(activity_type: 'Favourite', from_account: jeff, account: alice).count Notification.where(activity_type: 'Favourite', from_account: jeff, account: alice).count
}.from(1).to(0) }.from(1).to(0)
end end
end end
context 'when removed status is a private self-reblog' do context 'when removed status is a private self-reblog' do
before do let!(:original_status) { Fabricate(:status, account: alice, text: 'Hello ThisIsASecret', visibility: :private) }
@original_status = Fabricate(:status, account: alice, text: 'Hello ThisIsASecret', visibility: :private) let!(:status) { ReblogService.new.call(alice, original_status) }
@status = ReblogService.new.call(alice, @original_status)
end
it 'sends Undo activity to followers' do it 'sends Undo activity to followers' do
subject.call(@status) subject.call(status)
expect(a_request(:post, 'http://example.com/inbox').with( expect(a_request(:post, 'http://example.com/inbox').with(
body: hash_including({ body: hash_including({
'type' => 'Undo', 'type' => 'Undo',
'object' => hash_including({ 'object' => hash_including({
'type' => 'Announce', 'type' => 'Announce',
'object' => ActivityPub::TagManager.instance.uri_for(@original_status), 'object' => ActivityPub::TagManager.instance.uri_for(original_status),
}), }),
}) })
)).to have_been_made.once )).to have_been_made.once
@ -92,19 +91,17 @@ RSpec.describe RemoveStatusService, type: :service do
end end
context 'when removed status is public self-reblog' do context 'when removed status is public self-reblog' do
before do let!(:original_status) { Fabricate(:status, account: alice, text: 'Hello ThisIsASecret', visibility: :public) }
@original_status = Fabricate(:status, account: alice, text: 'Hello ThisIsASecret', visibility: :public) let!(:status) { ReblogService.new.call(alice, original_status) }
@status = ReblogService.new.call(alice, @original_status)
end
it 'sends Undo activity to followers' do it 'sends Undo activity to followers' do
subject.call(@status) subject.call(status)
expect(a_request(:post, 'http://example.com/inbox').with( expect(a_request(:post, 'http://example.com/inbox').with(
body: hash_including({ body: hash_including({
'type' => 'Undo', 'type' => 'Undo',
'object' => hash_including({ 'object' => hash_including({
'type' => 'Announce', 'type' => 'Announce',
'object' => ActivityPub::TagManager.instance.uri_for(@original_status), 'object' => ActivityPub::TagManager.instance.uri_for(original_status),
}), }),
}) })
)).to have_been_made.once )).to have_been_made.once

View file

@ -19,17 +19,15 @@ describe SearchService, type: :service do
end end
describe 'with an url query' do describe 'with an url query' do
before do let(:query) { 'http://test.host/query' }
@query = 'http://test.host/query'
end
context 'when it does not find anything' do context 'when it does not find anything' do
it 'returns the empty results' do it 'returns the empty results' do
service = instance_double(ResolveURLService, call: nil) service = instance_double(ResolveURLService, call: nil)
allow(ResolveURLService).to receive(:new).and_return(service) allow(ResolveURLService).to receive(:new).and_return(service)
results = subject.call(@query, nil, 10, resolve: true) results = subject.call(query, nil, 10, resolve: true)
expect(service).to have_received(:call).with(@query, on_behalf_of: nil) expect(service).to have_received(:call).with(query, on_behalf_of: nil)
expect(results).to eq empty_results expect(results).to eq empty_results
end end
end end
@ -40,8 +38,8 @@ describe SearchService, type: :service do
service = instance_double(ResolveURLService, call: account) service = instance_double(ResolveURLService, call: account)
allow(ResolveURLService).to receive(:new).and_return(service) allow(ResolveURLService).to receive(:new).and_return(service)
results = subject.call(@query, nil, 10, resolve: true) results = subject.call(query, nil, 10, resolve: true)
expect(service).to have_received(:call).with(@query, on_behalf_of: nil) expect(service).to have_received(:call).with(query, on_behalf_of: nil)
expect(results).to eq empty_results.merge(accounts: [account]) expect(results).to eq empty_results.merge(accounts: [account])
end end
end end
@ -52,8 +50,8 @@ describe SearchService, type: :service do
service = instance_double(ResolveURLService, call: status) service = instance_double(ResolveURLService, call: status)
allow(ResolveURLService).to receive(:new).and_return(service) allow(ResolveURLService).to receive(:new).and_return(service)
results = subject.call(@query, nil, 10, resolve: true) results = subject.call(query, nil, 10, resolve: true)
expect(service).to have_received(:call).with(@query, on_behalf_of: nil) expect(service).to have_received(:call).with(query, on_behalf_of: nil)
expect(results).to eq empty_results.merge(statuses: [status]) expect(results).to eq empty_results.merge(statuses: [status])
end end
end end

View file

@ -6,38 +6,36 @@ describe UnblockDomainService, type: :service do
subject { described_class.new } subject { described_class.new }
describe 'call' do describe 'call' do
before do let!(:independently_suspended) { Fabricate(:account, domain: 'example.com', suspended_at: 1.hour.ago) }
@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) }
@independently_silenced = Fabricate(:account, domain: 'example.com', silenced_at: 1.hour.ago) let!(:domain_block) { Fabricate(:domain_block, domain: 'example.com') }
@domain_block = Fabricate(:domain_block, domain: 'example.com') let!(:silenced) { Fabricate(:account, domain: 'example.com', silenced_at: domain_block.created_at) }
@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) }
@suspended = Fabricate(:account, domain: 'example.com', suspended_at: @domain_block.created_at)
end
it 'unsilences accounts and removes block' do it 'unsilences accounts and removes block' do
@domain_block.update(severity: :silence) domain_block.update(severity: :silence)
subject.call(@domain_block) subject.call(domain_block)
expect_deleted_domain_block expect_deleted_domain_block
expect(@silenced.reload.silenced?).to be false expect(silenced.reload.silenced?).to be false
expect(@suspended.reload.suspended?).to be true expect(suspended.reload.suspended?).to be true
expect(@independently_suspended.reload.suspended?).to be true expect(independently_suspended.reload.suspended?).to be true
expect(@independently_silenced.reload.silenced?).to be true expect(independently_silenced.reload.silenced?).to be true
end end
it 'unsuspends accounts and removes block' do it 'unsuspends accounts and removes block' do
@domain_block.update(severity: :suspend) domain_block.update(severity: :suspend)
subject.call(@domain_block) subject.call(domain_block)
expect_deleted_domain_block expect_deleted_domain_block
expect(@suspended.reload.suspended?).to be false expect(suspended.reload.suspended?).to be false
expect(@silenced.reload.silenced?).to be false expect(silenced.reload.silenced?).to be false
expect(@independently_suspended.reload.suspended?).to be true expect(independently_suspended.reload.suspended?).to be true
expect(@independently_silenced.reload.silenced?).to be true expect(independently_silenced.reload.silenced?).to be true
end end
end end
def expect_deleted_domain_block def expect_deleted_domain_block
expect { @domain_block.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { domain_block.reload }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end

View file

@ -41,3 +41,38 @@ class SearchDataManager
Tag.destroy_all Tag.destroy_all
end end
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, type: :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?
RUN_SEARCH_SPECS
end
end

View file

@ -76,3 +76,32 @@ class StreamingServerManager
@running_thread.join @running_thread.join
end end
end end
RSpec.configure do |config|
config.before :suite do
if streaming_examples_present?
# Compile assets
Webpacker.compile
# 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
private
def streaming_server_manager
@streaming_server_manager ||= StreamingServerManager.new
end
def streaming_examples_present?
RUN_SYSTEM_SPECS
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe BlacklistedEmailValidator, type: :validator do RSpec.describe BlacklistedEmailValidator do
describe '#validate' do describe '#validate' do
subject { described_class.new.validate(user); errors } subject { described_class.new.validate(user); errors }

View file

@ -2,7 +2,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe DisallowedHashtagsValidator, type: :validator do RSpec.describe DisallowedHashtagsValidator do
let(:disallowed_tags) { [] } let(:disallowed_tags) { [] }
describe '#validate' do describe '#validate' do

View file

@ -2,48 +2,76 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe FollowLimitValidator, type: :validator do RSpec.describe FollowLimitValidator do
describe '#validate' do describe '#validate' do
before do context 'with a nil account' do
allow_any_instance_of(described_class).to receive(:limit_reached?).with(account) do it 'does not add validation errors to base' do
limit_reached follow = Fabricate.build(:follow, account: nil)
end
described_class.new.validate(follow) follow.valid?
end
let(:follow) { instance_double(Follow, account: account, errors: errors) } expect(follow.errors[:base]).to be_empty
let(:errors) { instance_double(ActiveModel::Errors, add: nil) }
let(:account) { instance_double(Account, nil?: _nil, local?: local, following_count: 0, followers_count: 0) }
let(:_nil) { true }
let(:local) { false }
context 'with follow.account.nil? || !follow.account.local?' do
let(:_nil) { true }
it 'not calls errors.add' do
expect(errors).to_not have_received(:add).with(:base, any_args)
end end
end end
context 'with !(follow.account.nil? || !follow.account.local?)' do context 'with a non-local account' do
let(:_nil) { false } it 'does not add validation errors to base' do
let(:local) { true } follow = Fabricate.build(:follow, account: Account.new(domain: 'host.example'))
context 'when limit_reached?' do follow.valid?
let(:limit_reached) { true }
it 'calls errors.add' do expect(follow.errors[:base]).to be_empty
expect(errors).to have_received(:add) end
.with(:base, I18n.t('users.follow_limit_reached', limit: FollowLimitValidator::LIMIT)) 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
end end
context 'with !limit_reached?' do context 'when the following count is over the limit' do
let(:limit_reached) { false } before do
allow(account).to receive(:following_count).and_return(described_class::LIMIT + 100)
end
it 'not calls errors.add' do context 'when the followers count is low' do
expect(errors).to_not have_received(:add).with(:base, any_args) 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: FollowLimitValidator::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 end

View file

@ -2,7 +2,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe PollValidator, type: :validator do RSpec.describe PollValidator do
describe '#validate' do describe '#validate' do
before do before do
validator.validate(poll) validator.validate(poll)

View file

@ -2,7 +2,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe StatusPinValidator, type: :validator do RSpec.describe StatusPinValidator do
describe '#validate' do describe '#validate' do
before do before do
subject.validate(pin) subject.validate(pin)

31342
yarn.lock

File diff suppressed because it is too large Load diff