Merge commit '23e32a4b3031d1da8b911e0145d61b4dd47c4f96' into glitch-soc/merge-upstream

Conflicts:
- `Gemfile.lock`:
  Conflict because we had updated the `json-ld` gem to fix a yanked dependency.
  Kept our version of `json-ld` while updating other dependencies.
This commit is contained in:
Claire 2023-12-18 18:07:24 +01:00
commit bc33be0342
45 changed files with 791 additions and 1063 deletions

View File

@ -57,7 +57,6 @@ RSpec/AnyInstance:
- 'spec/controllers/activitypub/inboxes_controller_spec.rb' - 'spec/controllers/activitypub/inboxes_controller_spec.rb'
- 'spec/controllers/admin/accounts_controller_spec.rb' - 'spec/controllers/admin/accounts_controller_spec.rb'
- 'spec/controllers/admin/resets_controller_spec.rb' - 'spec/controllers/admin/resets_controller_spec.rb'
- 'spec/controllers/admin/settings/branding_controller_spec.rb'
- 'spec/controllers/auth/sessions_controller_spec.rb' - 'spec/controllers/auth/sessions_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb' - 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb' - 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb'

View File

@ -88,7 +88,7 @@ gem 'simple-navigation', '~> 4.4'
gem 'simple_form', '~> 5.2' gem 'simple_form', '~> 5.2'
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie' gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
gem 'stoplight', '~> 3.0.1' gem 'stoplight', '~> 3.0.1'
gem 'strong_migrations', '1.3.0' gem 'strong_migrations', '1.6.4'
gem 'tty-prompt', '~> 0.23', require: false gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0' gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2023' gem 'tzinfo-data', '~> 1.2023'
@ -195,7 +195,7 @@ gem 'xorcist', '~> 1.1'
gem 'cocoon', '~> 1.2' gem 'cocoon', '~> 1.2'
gem 'net-http', '~> 0.3.2' gem 'net-http', '~> 0.4.0'
gem 'rubyzip', '~> 2.3' gem 'rubyzip', '~> 2.3'
gem 'hcaptcha', '~> 7.1' gem 'hcaptcha', '~> 7.1'

View File

@ -130,8 +130,8 @@ GEM
attr_required (1.0.1) attr_required (1.0.1)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.809.0) aws-partitions (1.828.0)
aws-sdk-core (3.181.0) aws-sdk-core (3.183.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
@ -139,7 +139,7 @@ GEM
aws-sdk-kms (1.71.0) aws-sdk-kms (1.71.0)
aws-sdk-core (~> 3, >= 3.177.0) aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.133.0) aws-sdk-s3 (1.136.0)
aws-sdk-core (~> 3, >= 3.181.0) aws-sdk-core (~> 3, >= 3.181.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6) aws-sigv4 (~> 1.6)
@ -389,10 +389,10 @@ GEM
multi_json (~> 1.15) multi_json (~> 1.15)
rack (>= 2.2, < 4) rack (>= 2.2, < 4)
rdf (~> 3.3) rdf (~> 3.3)
json-ld-preloaded (3.2.2) json-ld-preloaded (3.3.0)
json-ld (~> 3.2) json-ld (~> 3.3)
rdf (~> 3.2) rdf (~> 3.3)
json-schema (4.0.0) json-schema (4.1.1)
addressable (>= 2.8) addressable (>= 2.8)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
jwt (2.7.1) jwt (2.7.1)
@ -454,13 +454,13 @@ GEM
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2023.0808) mime-types-data (3.2023.0808)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.4) mini_portile2 (2.8.5)
minitest (5.20.0) minitest (5.20.0)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.3.0)
mutex_m (0.1.2) mutex_m (0.1.2)
net-http (0.3.2) net-http (0.4.0)
uri uri
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
@ -689,13 +689,13 @@ GEM
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)
safety_net_attestation (0.4.0) safety_net_attestation (0.4.0)
jwt (~> 2.0) jwt (~> 2.0)
sanitize (6.0.2) sanitize (6.1.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
scenic (1.7.0) scenic (1.7.0)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
railties (>= 4.0.0) railties (>= 4.0.0)
selenium-webdriver (4.13.1) selenium-webdriver (4.15.0)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0) rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0) websocket (~> 1.0)
@ -710,7 +710,7 @@ GEM
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8) sidekiq (>= 6, < 8)
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.29) sidekiq-unique-jobs (7.1.30)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0) brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
redis (< 5.0) redis (< 5.0)
@ -718,7 +718,7 @@ GEM
thor (>= 0.20, < 3.0) thor (>= 0.20, < 3.0)
simple-navigation (4.4.0) simple-navigation (4.4.0)
activesupport (>= 2.3.2) activesupport (>= 2.3.2)
simple_form (5.2.0) simple_form (5.3.0)
actionpack (>= 5.2) actionpack (>= 5.2)
activemodel (>= 5.2) activemodel (>= 5.2)
simplecov (0.22.0) simplecov (0.22.0)
@ -740,7 +740,7 @@ GEM
stoplight (3.0.2) stoplight (3.0.2)
redlock (~> 1.0) redlock (~> 1.0)
stringio (3.0.8) stringio (3.0.8)
strong_migrations (1.3.0) strong_migrations (1.6.4)
activerecord (>= 5.2) activerecord (>= 5.2)
swd (1.3.0) swd (1.3.0)
activesupport (>= 3) activesupport (>= 3)
@ -753,7 +753,7 @@ GEM
terrapin (0.6.0) terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
test-prof (1.2.3) test-prof (1.2.3)
thor (1.2.2) thor (1.3.0)
tilt (2.3.0) tilt (2.3.0)
timeout (0.4.0) timeout (0.4.0)
tpm-key_attestation (0.12.0) tpm-key_attestation (0.12.0)
@ -883,7 +883,7 @@ DEPENDENCIES
md-paperclip-azure (~> 2.2) md-paperclip-azure (~> 2.2)
memory_profiler memory_profiler
mime-types (~> 3.5.0) mime-types (~> 3.5.0)
net-http (~> 0.3.2) net-http (~> 0.4.0)
net-ldap (~> 0.18) net-ldap (~> 0.18)
nokogiri (~> 1.15) nokogiri (~> 1.15)
nsa! nsa!
@ -941,7 +941,7 @@ DEPENDENCIES
sprockets-rails (~> 3.4) sprockets-rails (~> 3.4)
stackprof stackprof
stoplight (~> 3.0.1) stoplight (~> 3.0.1)
strong_migrations (= 1.3.0) strong_migrations (= 1.6.4)
test-prof test-prof
thor (~> 1.2) thor (~> 1.2)
tty-prompt (~> 0.23) tty-prompt (~> 0.23)

View File

@ -6,7 +6,7 @@ module Admin::AccountModerationNotesHelper
link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
safe_join([ safe_join([
image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), image_tag(account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar'),
content_tag(:span, account.acct, class: 'username'), content_tag(:span, account.acct, class: 'username'),
], ' ') ], ' ')
end end

View File

@ -25,7 +25,7 @@ module SettingsHelper
return if account.nil? return if account.nil?
link_to ActivityPub::TagManager.instance.url_for(account), class: 'name-tag', title: account.acct do link_to ActivityPub::TagManager.instance.url_for(account), class: 'name-tag', title: account.acct do
safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ') safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
end end
end end
end end

View File

@ -13,7 +13,7 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
} }
> >
<img <img
alt="alice" alt=""
src="/animated/alice.gif" src="/animated/alice.gif"
/> />
</div> </div>
@ -32,7 +32,7 @@ exports[`<Avatar /> Still renders a still avatar 1`] = `
} }
> >
<img <img
alt="alice" alt=""
src="/static/alice.jpg" src="/static/alice.jpg"
/> />
</div> </div>

View File

@ -42,7 +42,7 @@ export const Avatar: React.FC<Props> = ({
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
style={style} style={style}
> >
{src && <img src={src} alt={account?.get('acct')} />} {src && <img src={src} alt='' />}
</div> </div>
); );
}; };

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

@ -178,7 +178,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
modalType: 'IMAGE', modalType: 'IMAGE',
modalProps: { modalProps: {
src: account.get('avatar'), src: account.get('avatar'),
alt: account.get('acct'), alt: '',
}, },
})); }));
}, },

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

@ -19,8 +19,10 @@ import { throttle } from 'lodash';
import { Blurhash } from 'mastodon/components/blurhash'; import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
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({
@ -180,6 +182,7 @@ class Video extends PureComponent {
}; };
handlePause = () => { handlePause = () => {
this.video.pause();
this.setState({ paused: true }); this.setState({ paused: true });
}; };
@ -226,8 +229,8 @@ class Video extends PureComponent {
if(!isNaN(x)) { if(!isNaN(x)) {
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => { this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
this.video.volume = x; this._syncVideoToVolumeState(x);
this.video.muted = this.state.muted; this._saveVolumeState(x);
}); });
} }
}, 15); }, 15);
@ -343,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 = () => {
@ -365,6 +389,8 @@ class Video extends PureComponent {
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
window.addEventListener('scroll', this.handleScroll); window.addEventListener('scroll', this.handleScroll);
this._syncVideoFromLocalStorage();
} }
componentWillUnmount () { componentWillUnmount () {
@ -437,8 +463,24 @@ class Video extends PureComponent {
const muted = !(this.video.muted || this.state.volume === 0); const muted = !(this.video.muted || this.state.volume === 0);
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => { this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
this.video.volume = this.state.volume; this._syncVideoToVolumeState();
this.video.muted = this.state.muted; this._saveVolumeState();
});
};
_syncVideoToVolumeState = (volume = null, muted = null) => {
this.video.volume = volume ?? this.state.volume;
this.video.muted = muted ?? this.state.muted;
};
_saveVolumeState = (volume = null, muted = null) => {
playerSettings.set('volume', volume ?? this.state.volume);
playerSettings.set('muted', muted ?? this.state.muted);
};
_syncVideoFromLocalStorage = () => {
this.setState({ volume: playerSettings.get('volume') ?? 0.5, muted: playerSettings.get('muted') ?? false }, () => {
this._syncVideoToVolumeState();
}); });
}; };
@ -480,6 +522,7 @@ class Video extends PureComponent {
handleVolumeChange = () => { handleVolumeChange = () => {
this.setState({ volume: this.video.volume, muted: this.video.muted }); this.setState({ volume: this.video.volume, muted: this.video.muted });
this._saveVolumeState(this.video.volume, this.video.muted);
}; };
handleOpenVideo = () => { handleOpenVideo = () => {

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

@ -46,4 +46,5 @@ export default class Settings {
export const pushNotificationsSetting = new Settings('mastodon_push_notification_data'); export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
export const tagHistory = new Settings('mastodon_tag_history'); export const tagHistory = new Settings('mastodon_tag_history');
export const bannerSettings = new Settings('mastodon_banner_settings'); export const bannerSettings = new Settings('mastodon_banner_settings');
export const searchHistory = new Settings('mastodon_search_history'); export const searchHistory = new Settings('mastodon_search_history');
export const playerSettings = new Settings('mastodon_player');

View File

@ -20,7 +20,7 @@
.detailed-status__meta .detailed-status__meta
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'name-tag', target: '_blank', rel: 'noopener noreferrer' do = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'name-tag', target: '_blank', rel: 'noopener noreferrer' do
= image_tag(status.account.avatar.url, width: 15, height: 15, alt: display_name(status.account), class: 'avatar') = image_tag(status.account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar')
.username= status.account.acct .username= status.account.acct
· ·
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do = link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do

View File

@ -86,115 +86,6 @@ module Mastodon
# config.time_zone = "Central Time (US & Canada)" # config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras") # config.eager_load_paths << Rails.root.join("extras")
# All translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
config.i18n.available_locales = [
:af,
:an,
:ar,
:ast,
:be,
:bg,
:bn,
:br,
:bs,
:ca,
:ckb,
:co,
:cs,
:cy,
:da,
:de,
:el,
:en,
:'en-GB',
:eo,
:es,
:'es-AR',
:'es-MX',
:et,
:eu,
:fa,
:fi,
:fo,
:fr,
:'fr-QC',
:fy,
:ga,
:gd,
:gl,
:he,
:hi,
:hr,
:hu,
:hy,
:id,
:ig,
:io,
:is,
:it,
:ja,
:ka,
:kab,
:kk,
:kn,
:ko,
:ku,
:kw,
:la,
:lt,
:lv,
:mk,
:ml,
:mr,
:ms,
:my,
:nl,
:nn,
:no,
:oc,
:pa,
:pl,
:'pt-BR',
:'pt-PT',
:ro,
:ru,
:sa,
:sc,
:sco,
:si,
:sk,
:sl,
:sq,
:sr,
:'sr-Latn',
:sv,
:szl,
:ta,
:te,
:th,
:tr,
:tt,
:ug,
:uk,
:ur,
:vi,
:zgh,
:'zh-CN',
:'zh-HK',
:'zh-TW',
]
config.i18n.default_locale = begin
custom_default_locale = ENV['DEFAULT_LOCALE']&.to_sym
if config.i18n.available_locales.include?(custom_default_locale)
custom_default_locale
else
:en
end
end
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
# config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')] # config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]

110
config/initializers/i18n.rb Normal file
View File

@ -0,0 +1,110 @@
# frozen_string_literal: true
Rails.application.configure do
config.i18n.available_locales = [
:af,
:an,
:ar,
:ast,
:be,
:bg,
:bn,
:br,
:bs,
:ca,
:ckb,
:co,
:cs,
:cy,
:da,
:de,
:el,
:en,
:'en-GB',
:eo,
:es,
:'es-AR',
:'es-MX',
:et,
:eu,
:fa,
:fi,
:fo,
:fr,
:'fr-QC',
:fy,
:ga,
:gd,
:gl,
:he,
:hi,
:hr,
:hu,
:hy,
:id,
:ig,
:io,
:is,
:it,
:ja,
:ka,
:kab,
:kk,
:kn,
:ko,
:ku,
:kw,
:la,
:lt,
:lv,
:mk,
:ml,
:mr,
:ms,
:my,
:nl,
:nn,
:no,
:oc,
:pa,
:pl,
:'pt-BR',
:'pt-PT',
:ro,
:ru,
:sa,
:sc,
:sco,
:si,
:sk,
:sl,
:sq,
:sr,
:'sr-Latn',
:sv,
:szl,
:ta,
:te,
:th,
:tr,
:tt,
:ug,
:uk,
:ur,
:vi,
:zgh,
:'zh-CN',
:'zh-HK',
:'zh-TW',
]
config.i18n.default_locale = begin
custom_default_locale = ENV['DEFAULT_LOCALE']&.to_sym
if Rails.configuration.i18n.available_locales.include?(custom_default_locale)
custom_default_locale
else
:en
end
end
end

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
@ -91,10 +103,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

@ -136,24 +136,24 @@ module Mastodon::CLI
Mastodon has to be stopped to run this task, which will take a long time and may be destructive. Mastodon has to be stopped to run this task, which will take a long time and may be destructive.
LONG_DESC LONG_DESC
def fix_duplicates def fix_duplicates
if ActiveRecord::Migrator.current_version < MIN_SUPPORTED_VERSION verify_system_ready!
say 'Your version of the database schema is too old and is not supported by this script.', :red
say 'Please update to at least Mastodon 3.0.0 before running this script.', :red
exit(1)
elsif ActiveRecord::Migrator.current_version > MAX_SUPPORTED_VERSION
say 'Your version of the database schema is more recent than this script, this may cause unexpected errors.', :yellow
exit(1) unless yes?('Continue anyway? (Yes/No)')
end
if Sidekiq::ProcessSet.new.any? process_deduplications
say 'It seems Sidekiq is running. All Mastodon processes need to be stopped when using this script.', :red
exit(1)
end
say 'This task will take a long time to run and is potentially destructive.', :yellow deduplication_cleanup_tasks
say 'Please make sure to stop Mastodon and have a backup.', :yellow
exit(1) unless yes?('Continue? (Yes/No)')
say 'Finished!'
end
private
def verify_system_ready!
verify_schema_version!
verify_sidekiq_not_active!
verify_backup_warning!
end
def process_deduplications
deduplicate_users! deduplicate_users!
deduplicate_account_domain_blocks! deduplicate_account_domain_blocks!
deduplicate_account_identity_proofs! deduplicate_account_identity_proofs!
@ -173,14 +173,44 @@ module Mastodon::CLI
deduplicate_webauthn_credentials! deduplicate_webauthn_credentials!
deduplicate_webhooks! deduplicate_webhooks!
deduplicate_software_updates! deduplicate_software_updates!
Scenic.database.refresh_materialized_view('instances', concurrently: true, cascade: false) if ActiveRecord::Migrator.current_version >= 2020_12_06_004238
Rails.cache.clear
say 'Finished!'
end end
private def deduplication_cleanup_tasks
refresh_instances_view if schema_has_instances_view?
Rails.cache.clear
end
def refresh_instances_view
Scenic.database.refresh_materialized_view('instances', concurrently: true, cascade: false)
end
def schema_has_instances_view?
ActiveRecord::Migrator.current_version >= 2020_12_06_004238
end
def verify_schema_version!
if ActiveRecord::Migrator.current_version < MIN_SUPPORTED_VERSION
say 'Your version of the database schema is too old and is not supported by this script.', :red
say 'Please update to at least Mastodon 3.0.0 before running this script.', :red
exit(1)
elsif ActiveRecord::Migrator.current_version > MAX_SUPPORTED_VERSION
say 'Your version of the database schema is more recent than this script, this may cause unexpected errors.', :yellow
exit(1) unless yes?('Continue anyway? (Yes/No)')
end
end
def verify_sidekiq_not_active!
if Sidekiq::ProcessSet.new.any?
say 'It seems Sidekiq is running. All Mastodon processes need to be stopped when using this script.', :red
exit(1)
end
end
def verify_backup_warning!
say 'This task will take a long time to run and is potentially destructive.', :yellow
say 'Please make sure to stop Mastodon and have a backup.', :yellow
exit(1) unless yes?('Continue? (Yes/No)')
end
def deduplicate_accounts! def deduplicate_accounts!
remove_index_if_exists!(:accounts, 'index_accounts_on_username_and_domain_lower') remove_index_if_exists!(:accounts, 'index_accounts_on_username_and_domain_lower')

View File

@ -19,10 +19,6 @@ RSpec.describe Admin::Settings::BrandingController do
end end
describe 'PUT #update' do describe 'PUT #update' do
before do
allow_any_instance_of(Form::AdminSettings).to receive(:valid?).and_return(true)
end
around do |example| around do |example|
before = Setting.site_short_description before = Setting.site_short_description
Setting.site_short_description = nil Setting.site_short_description = nil

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

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

@ -8,13 +8,11 @@ describe EmojisController do
let(:emoji) { Fabricate(:custom_emoji) } let(:emoji) { Fabricate(:custom_emoji) }
describe 'GET #show' do describe 'GET #show' do
subject(:body) { JSON.parse(response.body, symbolize_names: true) }
let(:response) { get :show, params: { id: emoji.id, format: :json } } let(:response) { get :show, params: { id: emoji.id, format: :json } }
it 'returns the right response' do it 'returns the right response' do
expect(response).to have_http_status 200 expect(response).to have_http_status 200
expect(body[:name]).to eq ':coolcat:' expect(body_as_json[:name]).to eq ':coolcat:'
end end
end end
end end

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

@ -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

@ -3,8 +3,6 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe AccountStatusesFilter do RSpec.describe AccountStatusesFilter do
subject { described_class.new(account, current_account, params) }
let(:account) { Fabricate(:account) } let(:account) { Fabricate(:account) }
let(:current_account) { nil } let(:current_account) { nil }
let(:params) { {} } let(:params) { {} }
@ -38,6 +36,8 @@ RSpec.describe AccountStatusesFilter do
end end
describe '#results' do describe '#results' do
subject { described_class.new(account, current_account, params).results }
let(:tag) { Fabricate(:tag) } let(:tag) { Fabricate(:tag) }
before do before do
@ -56,7 +56,7 @@ RSpec.describe AccountStatusesFilter do
let(:params) { { only_media: true } } let(:params) { { only_media: true } }
it 'returns only statuses with media' do it 'returns only statuses with media' do
expect(subject.results.all?(&:with_media?)).to be true expect(subject.all?(&:with_media?)).to be true
end end
end end
@ -64,7 +64,7 @@ RSpec.describe AccountStatusesFilter do
let(:params) { { tagged: tag.name } } let(:params) { { tagged: tag.name } }
it 'returns only statuses with tag' do it 'returns only statuses with tag' do
expect(subject.results.all? { |s| s.tags.include?(tag) }).to be true expect(subject.all? { |s| s.tags.include?(tag) }).to be true
end end
end end
@ -72,7 +72,7 @@ RSpec.describe AccountStatusesFilter do
let(:params) { { exclude_replies: true } } let(:params) { { exclude_replies: true } }
it 'returns only statuses that are not replies' do it 'returns only statuses that are not replies' do
expect(subject.results.none?(&:reply?)).to be true expect(subject.none?(&:reply?)).to be true
end end
end end
@ -80,7 +80,7 @@ RSpec.describe AccountStatusesFilter do
let(:params) { { exclude_reblogs: true } } let(:params) { { exclude_reblogs: true } }
it 'returns only statuses that are not reblogs' do it 'returns only statuses that are not reblogs' do
expect(subject.results.none?(&:reblog?)).to be true expect(subject.none?(&:reblog?)).to be true
end end
end end
end end
@ -89,16 +89,12 @@ RSpec.describe AccountStatusesFilter do
let(:current_account) { nil } let(:current_account) { nil }
let(:direct_status) { nil } let(:direct_status) { nil }
it 'returns only public statuses' do it 'returns only public statuses, public replies, and public reblogs' do
expect(subject.results.pluck(:visibility).uniq).to match_array %w(unlisted public) expect(results_unique_visibilities).to match_array %w(unlisted public)
end
it 'returns public replies' do expect(results_in_reply_to_ids).to_not be_empty
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
end
it 'returns public reblogs' do expect(results_reblog_of_ids).to_not be_empty
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
end end
it_behaves_like 'filter params' it_behaves_like 'filter params'
@ -112,23 +108,19 @@ RSpec.describe AccountStatusesFilter do
end end
it 'returns nothing' do it 'returns nothing' do
expect(subject.results.to_a).to be_empty expect(subject.to_a).to be_empty
end end
end end
context 'when accessed by self' do context 'when accessed by self' do
let(:current_account) { account } let(:current_account) { account }
it 'returns everything' do it 'returns all statuses, replies, and reblogs' do
expect(subject.results.pluck(:visibility).uniq).to match_array %w(direct private unlisted public) expect(results_unique_visibilities).to match_array %w(direct private unlisted public)
end
it 'returns replies' do expect(results_in_reply_to_ids).to_not be_empty
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
end
it 'returns reblogs' do expect(results_reblog_of_ids).to_not be_empty
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
end end
it_behaves_like 'filter params' it_behaves_like 'filter params'
@ -141,23 +133,19 @@ RSpec.describe AccountStatusesFilter do
current_account.follow!(account) current_account.follow!(account)
end end
it 'returns private statuses' do it 'returns private statuses, replies, and reblogs' do
expect(subject.results.pluck(:visibility).uniq).to match_array %w(private unlisted public) expect(results_unique_visibilities).to match_array %w(private unlisted public)
end
it 'returns replies' do expect(results_in_reply_to_ids).to_not be_empty
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
end
it 'returns reblogs' do expect(results_reblog_of_ids).to_not be_empty
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
end end
context 'when there is a direct status mentioning the non-follower' do context 'when there is a direct status mentioning the non-follower' do
let!(:direct_status) { status_with_mention!(:direct, current_account) } let!(:direct_status) { status_with_mention!(:direct, current_account) }
it 'returns the direct status' do it 'returns the direct status' do
expect(subject.results.pluck(:id)).to include(direct_status.id) expect(results_ids).to include(direct_status.id)
end end
end end
@ -167,23 +155,19 @@ RSpec.describe AccountStatusesFilter do
context 'when accessed by a non-follower' do context 'when accessed by a non-follower' do
let(:current_account) { Fabricate(:account) } let(:current_account) { Fabricate(:account) }
it 'returns only public statuses' do it 'returns only public statuses, replies, and reblogs' do
expect(subject.results.pluck(:visibility).uniq).to match_array %w(unlisted public) expect(results_unique_visibilities).to match_array %w(unlisted public)
end
it 'returns public replies' do expect(results_in_reply_to_ids).to_not be_empty
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
end
it 'returns public reblogs' do expect(results_reblog_of_ids).to_not be_empty
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
end end
context 'when there is a private status mentioning the non-follower' do context 'when there is a private status mentioning the non-follower' do
let!(:private_status) { status_with_mention!(:private, current_account) } let!(:private_status) { status_with_mention!(:private, current_account) }
it 'returns the private status' do it 'returns the private status' do
expect(subject.results.pluck(:id)).to include(private_status.id) expect(results_ids).to include(private_status.id)
end end
end end
@ -195,7 +179,7 @@ RSpec.describe AccountStatusesFilter do
end end
it 'does not return reblog of blocked account' do it 'does not return reblog of blocked account' do
expect(subject.results.pluck(:id)).to_not include(reblog.id) expect(results_ids).to_not include(reblog.id)
end end
end end
@ -209,7 +193,7 @@ RSpec.describe AccountStatusesFilter do
end end
it 'does not return reblog of blocked domain' do it 'does not return reblog of blocked domain' do
expect(subject.results.pluck(:id)).to_not include(reblog.id) expect(results_ids).to_not include(reblog.id)
end end
end end
@ -223,7 +207,7 @@ RSpec.describe AccountStatusesFilter do
end end
it 'returns the reblog from the non-blocked domain' do it 'returns the reblog from the non-blocked domain' do
expect(subject.results.pluck(:id)).to include(reblog.id) expect(results_ids).to include(reblog.id)
end end
end end
@ -235,7 +219,7 @@ RSpec.describe AccountStatusesFilter do
end end
it 'does not return reblog of muted account' do it 'does not return reblog of muted account' do
expect(subject.results.pluck(:id)).to_not include(reblog.id) expect(results_ids).to_not include(reblog.id)
end end
end end
@ -247,11 +231,29 @@ RSpec.describe AccountStatusesFilter do
end end
it 'does not return reblog of blocked-by account' do it 'does not return reblog of blocked-by account' do
expect(subject.results.pluck(:id)).to_not include(reblog.id) expect(results_ids).to_not include(reblog.id)
end end
end end
it_behaves_like 'filter params' it_behaves_like 'filter params'
end end
private
def results_unique_visibilities
subject.pluck(:visibility).uniq
end
def results_in_reply_to_ids
subject.pluck(:in_reply_to_id)
end
def results_reblog_of_ids
subject.pluck(:reblog_of_id)
end
def results_ids
subject.pluck(:id)
end
end end
end end

View File

@ -4,9 +4,51 @@ require 'rails_helper'
require 'mastodon/cli/maintenance' require 'mastodon/cli/maintenance'
describe Mastodon::CLI::Maintenance do describe Mastodon::CLI::Maintenance do
let(:cli) { described_class.new }
describe '.exit_on_failure?' do describe '.exit_on_failure?' do
it 'returns true' do it 'returns true' do
expect(described_class.exit_on_failure?).to be true expect(described_class.exit_on_failure?).to be true
end end
end end
describe '#fix_duplicates' do
context 'when the database version is too old' do
before do
allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2000_01_01_000000) # Earlier than minimum
end
it 'Exits with error message' do
expect { cli.invoke :fix_duplicates }.to output(
a_string_including('is too old')
).to_stdout.and raise_error(SystemExit)
end
end
context 'when the database version is too new and the user does not continue' do
before do
allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2100_01_01_000000) # Later than maximum
allow(cli.shell).to receive(:yes?).with('Continue anyway? (Yes/No)').and_return(false).once
end
it 'Exits with error message' do
expect { cli.invoke :fix_duplicates }.to output(
a_string_including('more recent')
).to_stdout.and raise_error(SystemExit)
end
end
context 'when Sidekiq is running' do
before do
allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2022_01_01_000000) # Higher than minimum, lower than maximum
allow(Sidekiq::ProcessSet).to receive(:new).and_return [:process]
end
it 'Exits with error message' do
expect { cli.invoke :fix_duplicates }.to output(
a_string_including('Sidekiq is running')
).to_stdout.and raise_error(SystemExit)
end
end
end
end end

View File

@ -124,7 +124,7 @@ describe 'Caching behavior' do
expect(response.cookies).to be_empty expect(response.cookies).to be_empty
end end
it 'sets public cache control' do it 'sets public cache control', :aggregate_failures 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
@ -141,11 +141,8 @@ describe 'Caching behavior' do
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
@ -171,17 +168,24 @@ describe 'Caching behavior' do
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Moderator')) } let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Moderator')) }
before do before do
# rubocop:disable Style/NumericLiterals status = Fabricate(:status, account: alice, id: '110224538612341312')
status = Fabricate(:status, account: alice, id: 110224538612341312) Fabricate(:status, account: alice, id: '110224538643211312', visibility: :private)
Fabricate(:status, account: alice, id: 110224538643211312, visibility: :private)
Fabricate(:invite, code: 'abcdef') Fabricate(:invite, code: 'abcdef')
Fabricate(:poll, status: status, account: alice, id: 12345) Fabricate(:poll, status: status, account: alice, id: '12345')
# rubocop:enable Style/NumericLiterals
user.account.follow!(alice) user.account.follow!(alice)
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 }

View File

@ -3,13 +3,7 @@
require 'rails_helper' require 'rails_helper'
describe ActivityPub::DeviceSerializer do describe ActivityPub::DeviceSerializer do
let(:serialization) do let(:serialization) { serialized_record_json(record, described_class) }
JSON.parse(
ActiveModelSerializers::SerializableResource.new(
record, serializer: described_class
).to_json
)
end
let(:record) { Fabricate(:device) } let(:record) { Fabricate(:device) }
describe 'type' do describe 'type' do

View File

@ -3,7 +3,7 @@
require 'rails_helper' require 'rails_helper'
describe ActivityPub::NoteSerializer do describe ActivityPub::NoteSerializer do
subject { JSON.parse(@serialization.to_json) } subject { serialized_record_json(parent, described_class, adapter: ActivityPub::Adapter) }
let!(:account) { Fabricate(:account) } let!(:account) { Fabricate(:account) }
let!(:other) { Fabricate(:account) } let!(:other) { Fabricate(:account) }
@ -14,10 +14,6 @@ describe ActivityPub::NoteSerializer do
let!(:reply_by_account_third) { Fabricate(:status, account: account, thread: parent, visibility: :public) } let!(:reply_by_account_third) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) } let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) }
before do
@serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: described_class, adapter: ActivityPub::Adapter)
end
it 'has the expected shape' do it 'has the expected shape' do
expect(subject).to include({ expect(subject).to include({
'@context' => include('https://www.w3.org/ns/activitystreams'), '@context' => include('https://www.w3.org/ns/activitystreams'),

View File

@ -3,13 +3,7 @@
require 'rails_helper' require 'rails_helper'
describe ActivityPub::OneTimeKeySerializer do describe ActivityPub::OneTimeKeySerializer do
let(:serialization) do let(:serialization) { serialized_record_json(record, described_class) }
JSON.parse(
ActiveModelSerializers::SerializableResource.new(
record, serializer: described_class
).to_json
)
end
let(:record) { Fabricate(:one_time_key) } let(:record) { Fabricate(:one_time_key) }
describe 'type' do describe 'type' do

View File

@ -3,13 +3,7 @@
require 'rails_helper' require 'rails_helper'
describe ActivityPub::UndoLikeSerializer do describe ActivityPub::UndoLikeSerializer do
let(:serialization) do let(:serialization) { serialized_record_json(record, described_class) }
JSON.parse(
ActiveModelSerializers::SerializableResource.new(
record, serializer: described_class
).to_json
)
end
let(:record) { Fabricate(:favourite) } let(:record) { Fabricate(:favourite) }
describe 'type' do describe 'type' do

View File

@ -3,16 +3,12 @@
require 'rails_helper' require 'rails_helper'
describe ActivityPub::UpdatePollSerializer do describe ActivityPub::UpdatePollSerializer do
subject { JSON.parse(@serialization.to_json) } subject { serialized_record_json(status, described_class, adapter: ActivityPub::Adapter) }
let(:account) { Fabricate(:account) } let(:account) { Fabricate(:account) }
let(:poll) { Fabricate(:poll, account: account) } let(:poll) { Fabricate(:poll, account: account) }
let!(:status) { Fabricate(:status, account: account, poll: poll) } let!(:status) { Fabricate(:status, account: account, poll: poll) }
before do
@serialization = ActiveModelSerializers::SerializableResource.new(status, serializer: described_class, adapter: ActivityPub::Adapter)
end
it 'has a Update type' do it 'has a Update type' do
expect(subject['type']).to eql('Update') expect(subject['type']).to eql('Update')
end end

View File

@ -3,13 +3,7 @@
require 'rails_helper' require 'rails_helper'
describe ActivityPub::VoteSerializer do describe ActivityPub::VoteSerializer do
let(:serialization) do let(:serialization) { serialized_record_json(record, described_class) }
JSON.parse(
ActiveModelSerializers::SerializableResource.new(
record, serializer: described_class
).to_json
)
end
let(:record) { Fabricate(:poll_vote) } let(:record) { Fabricate(:poll_vote) }
describe 'type' do describe 'type' do

View File

@ -3,7 +3,7 @@
require 'rails_helper' require 'rails_helper'
describe REST::AccountSerializer do describe REST::AccountSerializer do
subject { JSON.parse(ActiveModelSerializers::SerializableResource.new(account, serializer: described_class).to_json) } subject { serialized_record_json(account, described_class) }
let(:role) { Fabricate(:user_role, name: 'Role', highlighted: true) } let(:role) { Fabricate(:user_role, name: 'Role', highlighted: true) }
let(:user) { Fabricate(:user, role: role) } let(:user) { Fabricate(:user, role: role) }

View File

@ -3,13 +3,7 @@
require 'rails_helper' require 'rails_helper'
describe REST::EncryptedMessageSerializer do describe REST::EncryptedMessageSerializer do
let(:serialization) do let(:serialization) { serialized_record_json(record, described_class) }
JSON.parse(
ActiveModelSerializers::SerializableResource.new(
record, serializer: described_class
).to_json
)
end
let(:record) { Fabricate(:encrypted_message) } let(:record) { Fabricate(:encrypted_message) }
describe 'account' do describe 'account' do

View File

@ -3,13 +3,7 @@
require 'rails_helper' require 'rails_helper'
describe REST::InstanceSerializer do describe REST::InstanceSerializer do
let(:serialization) do let(:serialization) { serialized_record_json(record, described_class) }
JSON.parse(
ActiveModelSerializers::SerializableResource.new(
record, serializer: described_class
).to_json
)
end
let(:record) { InstancePresenter.new } let(:record) { InstancePresenter.new }
describe 'usage' do describe 'usage' do

View File

@ -3,13 +3,7 @@
require 'rails_helper' require 'rails_helper'
describe REST::Keys::ClaimResultSerializer do describe REST::Keys::ClaimResultSerializer do
let(:serialization) do let(:serialization) { serialized_record_json(record, described_class) }
JSON.parse(
ActiveModelSerializers::SerializableResource.new(
record, serializer: described_class
).to_json
)
end
let(:record) { Keys::ClaimService::Result.new(Account.new(id: 123), 456) } let(:record) { Keys::ClaimService::Result.new(Account.new(id: 123), 456) }
describe 'account' do describe 'account' do

View File

@ -3,13 +3,7 @@
require 'rails_helper' require 'rails_helper'
describe REST::Keys::DeviceSerializer do describe REST::Keys::DeviceSerializer do
let(:serialization) do let(:serialization) { serialized_record_json(record, described_class) }
JSON.parse(
ActiveModelSerializers::SerializableResource.new(
record, serializer: described_class
).to_json
)
end
let(:record) { Device.new(name: 'Device name') } let(:record) { Device.new(name: 'Device name') }
describe 'name' do describe 'name' do

View File

@ -3,13 +3,7 @@
require 'rails_helper' require 'rails_helper'
describe REST::Keys::QueryResultSerializer do describe REST::Keys::QueryResultSerializer do
let(:serialization) do let(:serialization) { serialized_record_json(record, described_class) }
JSON.parse(
ActiveModelSerializers::SerializableResource.new(
record, serializer: described_class
).to_json
)
end
let(:record) { Keys::QueryService::Result.new(Account.new(id: 123), []) } let(:record) { Keys::QueryService::Result.new(Account.new(id: 123), []) }
describe 'account' do describe 'account' do

View File

@ -3,13 +3,7 @@
require 'rails_helper' require 'rails_helper'
describe REST::SuggestionSerializer do describe REST::SuggestionSerializer do
let(:serialization) do let(:serialization) { serialized_record_json(record, described_class) }
JSON.parse(
ActiveModelSerializers::SerializableResource.new(
record, serializer: described_class
).to_json
)
end
let(:record) do let(:record) do
AccountSuggestions::Suggestion.new( AccountSuggestions::Suggestion.new(
account: account, account: account,

View File

@ -52,6 +52,17 @@ def json_str_to_hash(str)
JSON.parse(str, symbolize_names: true) JSON.parse(str, symbolize_names: true)
end end
def serialized_record_json(record, serializer, adapter: nil)
options = { serializer: serializer }
options[:adapter] = adapter if adapter.present?
JSON.parse(
ActiveModelSerializers::SerializableResource.new(
record,
options
).to_json
)
end
def expect_push_bulk_to_match(klass, matcher) def expect_push_bulk_to_match(klass, matcher)
allow(Sidekiq::Client).to receive(:push_bulk) allow(Sidekiq::Client).to receive(:push_bulk)
yield yield
@ -60,122 +71,3 @@ def expect_push_bulk_to_match(klass, matcher)
'args' => matcher, 'args' => matcher,
})) }))
end end
class StreamingServerManager
@running_thread = nil
def initialize
at_exit { stop }
end
def start(port: 4020)
return if @running_thread
queue = Queue.new
@queue = queue
@running_thread = Thread.new do
Open3.popen2e(
{
'REDIS_NAMESPACE' => ENV.fetch('REDIS_NAMESPACE'),
'DB_NAME' => "#{ENV.fetch('DB_NAME', 'mastodon')}_test#{ENV.fetch('TEST_ENV_NUMBER', '')}",
'RAILS_ENV' => ENV.fetch('RAILS_ENV', 'test'),
'NODE_ENV' => ENV.fetch('STREAMING_NODE_ENV', 'development'),
'PORT' => port.to_s,
},
'node index.js', # must not call yarn here, otherwise it will fail because yarn does not send signals to its child process
chdir: Rails.root.join('streaming')
) do |_stdin, stdout_err, process_thread|
status = :starting
# Spawn a thread to listen on streaming server output
output_thread = Thread.new do
stdout_err.each_line do |line|
Rails.logger.info "Streaming server: #{line}"
if status == :starting && line.match('Streaming API now listening on')
status = :started
@queue.enq 'started'
end
end
end
# And another thread to listen on commands from the main thread
loop do
msg = queue.pop
case msg
when 'stop'
# we need to properly stop the reading thread
output_thread.kill
# Then stop the node process
Process.kill('KILL', process_thread.pid)
# And we stop ourselves
@running_thread.kill
end
end
end
end
# wait for 10 seconds for the streaming server to start
Timeout.timeout(10) do
loop do
break if @queue.pop == 'started'
end
end
end
def stop
return unless @running_thread
@queue.enq 'stop'
# Wait for the thread to end
@running_thread.join
end
end
class SearchDataManager
def prepare_test_data
4.times do |i|
username = "search_test_account_#{i}"
account = Fabricate.create(:account, username: username, indexable: i.even?, discoverable: i.even?, note: "Lover of #{i}.")
2.times do |j|
Fabricate.create(:status, account: account, text: "#{username}'s #{j} post", visibility: j.even? ? :public : :private)
end
end
3.times do |i|
Fabricate.create(:tag, name: "search_test_tag_#{i}")
end
end
def indexes
[
AccountsIndex,
PublicStatusesIndex,
StatusesIndex,
TagsIndex,
]
end
def populate_indexes
indexes.each do |index_class|
index_class.purge!
index_class.import!
end
end
def remove_indexes
indexes.each(&:delete!)
end
def cleanup_test_data
Status.destroy_all
Account.destroy_all
Tag.destroy_all
end
end

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
class SearchDataManager
def prepare_test_data
4.times do |i|
username = "search_test_account_#{i}"
account = Fabricate.create(:account, username: username, indexable: i.even?, discoverable: i.even?, note: "Lover of #{i}.")
2.times do |j|
Fabricate.create(:status, account: account, text: "#{username}'s #{j} post", visibility: j.even? ? :public : :private)
end
end
3.times do |i|
Fabricate.create(:tag, name: "search_test_tag_#{i}")
end
end
def indexes
[
AccountsIndex,
PublicStatusesIndex,
StatusesIndex,
TagsIndex,
]
end
def populate_indexes
indexes.each do |index_class|
index_class.purge!
index_class.import!
end
end
def remove_indexes
indexes.each(&:delete!)
end
def cleanup_test_data
Status.destroy_all
Account.destroy_all
Tag.destroy_all
end
end

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
class StreamingServerManager
@running_thread = nil
def initialize
at_exit { stop }
end
def start(port: 4020)
return if @running_thread
queue = Queue.new
@queue = queue
@running_thread = Thread.new do
Open3.popen2e(
{
'REDIS_NAMESPACE' => ENV.fetch('REDIS_NAMESPACE'),
'DB_NAME' => "#{ENV.fetch('DB_NAME', 'mastodon')}_test#{ENV.fetch('TEST_ENV_NUMBER', '')}",
'RAILS_ENV' => ENV.fetch('RAILS_ENV', 'test'),
'NODE_ENV' => ENV.fetch('STREAMING_NODE_ENV', 'development'),
'PORT' => port.to_s,
},
'node index.js', # must not call yarn here, otherwise it will fail because yarn does not send signals to its child process
chdir: Rails.root.join('streaming')
) do |_stdin, stdout_err, process_thread|
status = :starting
# Spawn a thread to listen on streaming server output
output_thread = Thread.new do
stdout_err.each_line do |line|
Rails.logger.info "Streaming server: #{line}"
if status == :starting && line.match('Streaming API now listening on')
status = :started
@queue.enq 'started'
end
end
end
# And another thread to listen on commands from the main thread
loop do
msg = queue.pop
case msg
when 'stop'
# we need to properly stop the reading thread
output_thread.kill
# Then stop the node process
Process.kill('KILL', process_thread.pid)
# And we stop ourselves
@running_thread.kill
end
end
end
end
# wait for 10 seconds for the streaming server to start
Timeout.timeout(10) do
loop do
break if @queue.pop == 'started'
end
end
end
def stop
return unless @running_thread
@queue.enq 'stop'
# Wait for the thread to end
@running_thread.join
end
end

758
yarn.lock

File diff suppressed because it is too large Load Diff