mirror of
https://github.com/yingziwu/mastodon.git
synced 2026-02-04 03:25:14 +00:00
Merge tag 'v4.2.23'
This commit is contained in:
commit
8c8b6b6c6a
24 changed files with 252 additions and 52 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -2,6 +2,31 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.2.23] - 2025-07-23
|
||||
|
||||
### Security
|
||||
|
||||
- Updated dependencies
|
||||
|
||||
## [4.2.22] - 2025-07-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Change passthrough video processing to emit `moov` atom at start of video (#34726 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix `NoMethodError` in edge case of emoji cache handling (#34749 by @dariusk)
|
||||
- Fix error when viewing statuses to deleted replies in moderation view (#32986 by @ClearlyClaire)
|
||||
- Fix search operators sometimes getting lost (#35190 by @ClearlyClaire)
|
||||
- Fix handling of remote attachments with multiple media types (#34996 by @ClearlyClaire)
|
||||
- Fix inconsistent filtering of silenced accounts for other silenced accounts (#34863 by @ClearlyClaire)
|
||||
- Fix handling of inlined `featured` collections in ActivityPub actor objects (#34789 and #34811 by @ClearlyClaire)
|
||||
- Fix admin dashboard crash on specific Elasticsearch connection errors (#34683 by @ClearlyClaire)
|
||||
- Fix OIDC account creation failing for long display names (#34639 by @defnull)
|
||||
- Fix `/share` not using server-set characters limit (#33459 by @kescherCode)
|
||||
- Fix wrong video dimensions for some rotated videos (#33008 and #33261 by @Gargron and @tribela)
|
||||
|
||||
## [4.2.21] - 2025-05-06
|
||||
|
||||
### Security
|
||||
|
|
|
|||
|
|
@ -446,7 +446,7 @@ GEM
|
|||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0808)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.8)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.19.0)
|
||||
msgpack (1.7.1)
|
||||
multi_json (1.15.0)
|
||||
|
|
@ -469,7 +469,7 @@ GEM
|
|||
net-protocol
|
||||
net-ssh (7.1.0)
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.8)
|
||||
nokogiri (1.18.9)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nsa (0.3.0)
|
||||
|
|
@ -533,7 +533,7 @@ GEM
|
|||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.13)
|
||||
rack (2.2.17)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.2)
|
||||
|
|
@ -741,7 +741,7 @@ GEM
|
|||
terrapin (0.6.0)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
test-prof (1.2.3)
|
||||
thor (1.3.2)
|
||||
thor (1.4.0)
|
||||
tilt (2.2.0)
|
||||
timeout (0.4.3)
|
||||
tpm-key_attestation (0.12.0)
|
||||
|
|
|
|||
11
SECURITY.md
11
SECURITY.md
|
|
@ -13,8 +13,9 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
|||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | --------- |
|
||||
| 4.3.x | Yes |
|
||||
| 4.2.x | Yes |
|
||||
| < 4.2 | No |
|
||||
| Version | Supported |
|
||||
| ------- | ---------------- |
|
||||
| 4.4.x | Yes |
|
||||
| 4.3.x | Yes |
|
||||
| 4.2.x | Until 2026-01-08 |
|
||||
| < 4.2 | No |
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ module JsonLdHelper
|
|||
# The url attribute can be a string, an array of strings, or an array of objects.
|
||||
# The objects could include a mimeType. Not-included mimeType means it's text/html.
|
||||
def url_to_href(value, preferred_type = nil)
|
||||
value = [value] if value.is_a?(Hash)
|
||||
|
||||
single_value = if value.is_a?(Array) && !value.first.is_a?(String)
|
||||
value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
|
||||
elsif value.is_a?(Array)
|
||||
|
|
@ -41,6 +43,15 @@ module JsonLdHelper
|
|||
end
|
||||
end
|
||||
|
||||
def url_to_media_type(value, preferred_type = nil)
|
||||
value = [value] if value.is_a?(Hash)
|
||||
return unless value.is_a?(Array) && !value.first.is_a?(String)
|
||||
|
||||
single_value = value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
|
||||
|
||||
single_value['mediaType'] unless single_value.nil?
|
||||
end
|
||||
|
||||
def as_array(value)
|
||||
if value.nil?
|
||||
[]
|
||||
|
|
|
|||
|
|
@ -3,18 +3,19 @@ import { PureComponent } from 'react';
|
|||
import { Provider } from 'react-redux';
|
||||
|
||||
import { fetchCustomEmojis } from '../actions/custom_emojis';
|
||||
import { fetchServer } from '../actions/server';
|
||||
import { hydrateStore } from '../actions/store';
|
||||
import Compose from '../features/standalone/compose';
|
||||
import initialState from '../initial_state';
|
||||
import { IntlProvider } from '../locales';
|
||||
import { store } from '../store';
|
||||
|
||||
|
||||
if (initialState) {
|
||||
store.dispatch(hydrateStore(initialState));
|
||||
}
|
||||
|
||||
store.dispatch(fetchCustomEmojis());
|
||||
store.dispatch(fetchServer());
|
||||
|
||||
export default class ComposeContainer extends PureComponent {
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class ActivityPub::Parser::MediaAttachmentParser
|
|||
end
|
||||
|
||||
def remote_url
|
||||
url = Addressable::URI.parse(@json['url'])&.normalize&.to_s
|
||||
url = Addressable::URI.parse(url_to_href(@json['url']))&.normalize&.to_s
|
||||
url unless unsupported_uri_scheme?(url)
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
nil
|
||||
|
|
@ -43,7 +43,7 @@ class ActivityPub::Parser::MediaAttachmentParser
|
|||
end
|
||||
|
||||
def file_content_type
|
||||
@json['mediaType']
|
||||
@json['mediaType'] || url_to_media_type(@json['url'])
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
|
|||
return true unless Chewy.enabled?
|
||||
|
||||
running_version.present? && compatible_version? && cluster_health['status'] == 'green' && indexes_match? && preset_matches?
|
||||
rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error
|
||||
rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error, HTTPClient::KeepAliveDisconnected
|
||||
false
|
||||
end
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
|
|||
else
|
||||
Admin::SystemCheck::Message.new(:elasticsearch_preset, nil, 'https://docs.joinmastodon.org/admin/elasticsearch/#scaling')
|
||||
end
|
||||
rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error
|
||||
rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error, HTTPClient::KeepAliveDisconnected
|
||||
Admin::SystemCheck::Message.new(:elasticsearch_running_check)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ class EntityCache
|
|||
uncached_ids << shortcode unless cached.key?(to_key(:emoji, shortcode, domain))
|
||||
end
|
||||
|
||||
unless uncached_ids.empty?
|
||||
if uncached_ids.empty?
|
||||
uncached = {}
|
||||
else
|
||||
uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain, disabled: false).index_by(&:shortcode)
|
||||
uncached.each_value { |item| Rails.cache.write(to_key(:emoji, item.shortcode, domain), item, expires_in: MAX_EXPIRATION) }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class SearchQueryTransformer < Parslet::Transform
|
|||
private
|
||||
|
||||
def clauses_by_operator
|
||||
@clauses_by_operator ||= @clauses.compact.chunk(&:operator).to_h
|
||||
@clauses_by_operator ||= @clauses.compact.group_by(&:operator)
|
||||
end
|
||||
|
||||
def flags_from_clauses!
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class StatusFilter
|
|||
end
|
||||
|
||||
def silenced_account?
|
||||
!account&.silenced? && status_account_silenced? && !account_following_status_account?
|
||||
status_account_silenced? && !account_following_status_account?
|
||||
end
|
||||
|
||||
def status_account_silenced?
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ class VideoMetadataExtractor
|
|||
# For some video streams the frame_rate reported by `ffprobe` will be 0/0, but for these streams we
|
||||
# should use `r_frame_rate` instead. Video screencast generated by Gnome Screencast have this issue.
|
||||
@frame_rate ||= @r_frame_rate
|
||||
# If the video has not been re-encoded by ffmpeg, it may contain rotation information,
|
||||
# and we need to simulate applying it to the dimensions
|
||||
@width, @height = @height, @width if video_stream[:side_data_list]&.any? { |x| x[:rotation]&.abs == 90 }
|
||||
end
|
||||
|
||||
if (audio_stream = audio_streams.first)
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ class Account < ApplicationRecord
|
|||
MENTION_RE = %r{(?<![=/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]]+([.-]+[[:word:]]+)*)?)}
|
||||
URL_PREFIX_RE = %r{\Ahttp(s?)://[^/]+}
|
||||
USERNAME_ONLY_RE = /\A#{USERNAME_RE}\z/i
|
||||
DISPLAY_NAME_LENGTH_LIMIT = 30
|
||||
|
||||
include Attachmentable
|
||||
include AccountAssociations
|
||||
|
|
@ -99,7 +100,7 @@ class Account < ApplicationRecord
|
|||
# Local user validations
|
||||
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' }
|
||||
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' }
|
||||
validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? }
|
||||
validates :display_name, length: { maximum: DISPLAY_NAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_display_name? }
|
||||
validates :note, note_length: { maximum: 500 }, if: -> { local? && will_save_change_to_note? }
|
||||
validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? }
|
||||
validates :uri, absence: true, if: :local?, on: :create
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ module Omniauthable
|
|||
external: true,
|
||||
account_attributes: {
|
||||
username: ensure_unique_username(ensure_valid_username(auth.uid)),
|
||||
display_name: auth.info.full_name || auth.info.name || [auth.info.first_name, auth.info.last_name].join(' '),
|
||||
display_name: display_name_from_auth(auth),
|
||||
},
|
||||
}
|
||||
end
|
||||
|
|
@ -121,5 +121,10 @@ module Omniauthable
|
|||
temp_username = starting_username.gsub(/[^a-z0-9_]+/i, '')
|
||||
temp_username.truncate(30, omission: '')
|
||||
end
|
||||
|
||||
def display_name_from_auth(auth)
|
||||
display_name = auth.info.full_name || auth.info.name || [auth.info.first_name, auth.info.last_name].join(' ')
|
||||
display_name.truncate(Account::DISPLAY_NAME_LENGTH_LIMIT, omission: '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ class MediaAttachment < ApplicationRecord
|
|||
output: {
|
||||
'loglevel' => 'fatal',
|
||||
'map_metadata' => '-1',
|
||||
'movflags' => 'faststart', # Move metadata to start of file so playback can begin before download finishes
|
||||
'c:v' => 'copy',
|
||||
'c:a' => 'copy',
|
||||
}.freeze,
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
|
|||
include JsonLdHelper
|
||||
|
||||
def call(account, **options)
|
||||
return if account.featured_collection_url.blank? || account.suspended? || account.local?
|
||||
return if (account.featured_collection_url.blank? && options[:collection].blank?) || account.suspended? || account.local?
|
||||
|
||||
@account = account
|
||||
@options = options
|
||||
@json = fetch_resource(@account.featured_collection_url, true, local_follower)
|
||||
|
||||
return unless supported_context?(@json)
|
||||
@json = fetch_collection(options[:collection].presence || @account.featured_collection_url)
|
||||
return if @json.blank?
|
||||
|
||||
process_items(collection_items(@json))
|
||||
end
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
after_suspension_change! if suspension_changed?
|
||||
|
||||
unless @options[:only_key] || @account.suspended?
|
||||
check_featured_collection! if @account.featured_collection_url.present?
|
||||
check_featured_collection! if @json['featured'].present?
|
||||
check_featured_tags_collection! if @json['featuredTags'].present?
|
||||
check_links! if @account.fields.any?(&:requires_verification?)
|
||||
end
|
||||
|
|
@ -121,7 +121,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
end
|
||||
|
||||
def set_immediate_attributes!
|
||||
@account.featured_collection_url = @json['featured'] || ''
|
||||
@account.featured_collection_url = valid_collection_uri(@json['featured'])
|
||||
@account.devices_url = @json['devices'] || ''
|
||||
@account.display_name = @json['name'] || ''
|
||||
@account.note = @json['summary'] || ''
|
||||
|
|
@ -186,7 +186,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
end
|
||||
|
||||
def check_featured_collection!
|
||||
ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id, { 'hashtag' => @json['featuredTags'].blank?, 'request_id' => @options[:request_id] })
|
||||
ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id, { 'hashtag' => @json['featuredTags'].blank?, 'collection' => @json['featured'], 'request_id' => @options[:request_id] })
|
||||
end
|
||||
|
||||
def check_featured_tags_collection!
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
- if @status.reply?
|
||||
%tr
|
||||
%th= t('admin.statuses.in_reply_to')
|
||||
%td= admin_account_link_to @status.in_reply_to_account, path: admin_account_status_path(@status.thread.account_id, @status.in_reply_to_id)
|
||||
%td= admin_account_link_to @status.in_reply_to_account, path: @status.thread.present? ? admin_account_status_path(@status.thread.account_id, @status.in_reply_to_id) : nil
|
||||
%tr
|
||||
%th= t('admin.statuses.application')
|
||||
%td= @status.application&.name
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class ActivityPub::SynchronizeFeaturedCollectionWorker
|
|||
sidekiq_options queue: 'pull', lock: :until_executed, lock_ttl: 1.day.to_i
|
||||
|
||||
def perform(account_id, options = {})
|
||||
options = { note: true, hashtag: false }.deep_merge(options.deep_symbolize_keys)
|
||||
options = { note: true, hashtag: false }.deep_merge(options.symbolize_keys)
|
||||
|
||||
ActivityPub::FetchFeaturedCollectionService.new.call(Account.find(account_id), **options)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ services:
|
|||
|
||||
web:
|
||||
build: .
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.21
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.23
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
||||
|
|
@ -77,7 +77,7 @@ services:
|
|||
|
||||
streaming:
|
||||
build: .
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.21
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.23
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: node ./streaming
|
||||
|
|
@ -95,7 +95,7 @@ services:
|
|||
|
||||
sidekiq:
|
||||
build: .
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.21
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.23
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec sidekiq
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ module Mastodon
|
|||
end
|
||||
|
||||
def patch
|
||||
21
|
||||
23
|
||||
end
|
||||
|
||||
def default_prerelease
|
||||
|
|
|
|||
|
|
@ -4,29 +4,60 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe ActivityPub::Activity::Remove do
|
||||
let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') }
|
||||
let(:status) { Fabricate(:status, account: sender) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Add',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
||||
target: sender.featured_collection_url,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
StatusPin.create!(account: sender, status: status)
|
||||
subject.perform
|
||||
context 'when removing a pinned status' do
|
||||
let(:status) { Fabricate(:status, account: sender) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Remove',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
||||
target: sender.featured_collection_url,
|
||||
}.deep_stringify_keys
|
||||
end
|
||||
|
||||
before do
|
||||
StatusPin.create!(account: sender, status: status)
|
||||
end
|
||||
|
||||
it 'removes a pin' do
|
||||
expect { subject.perform }
|
||||
.to change { sender.pinned?(status) }.to(false)
|
||||
end
|
||||
end
|
||||
|
||||
it 'removes a pin' do
|
||||
expect(sender.pinned?(status)).to be false
|
||||
context 'when removing a featured tag' do
|
||||
let(:tag) { Fabricate(:tag) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Remove',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: {
|
||||
type: 'Hashtag',
|
||||
name: "##{tag.display_name}",
|
||||
href: "https://example.com/tags/#{tag.name}",
|
||||
},
|
||||
target: sender.featured_collection_url,
|
||||
}.deep_stringify_keys
|
||||
end
|
||||
|
||||
before do
|
||||
sender.featured_tags.find_or_create_by!(name: tag.name)
|
||||
end
|
||||
|
||||
it 'removes a pin' do
|
||||
expect { subject.perform }
|
||||
.to change { sender.featured_tags.exists?(tag: tag) }.to(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
51
spec/lib/activitypub/parser/media_attachment_parser_spec.rb
Normal file
51
spec/lib/activitypub/parser/media_attachment_parser_spec.rb
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Parser::MediaAttachmentParser do
|
||||
subject { described_class.new(json) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'Document',
|
||||
mediaType: 'image/png',
|
||||
url: 'http://example.com/attachment.png',
|
||||
}.deep_stringify_keys
|
||||
end
|
||||
|
||||
it 'correctly parses media attachment' do
|
||||
expect(subject).to have_attributes(
|
||||
remote_url: 'http://example.com/attachment.png',
|
||||
file_content_type: 'image/png'
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the URL is a link with multiple options' do
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'Document',
|
||||
url: [
|
||||
{
|
||||
type: 'Link',
|
||||
mediaType: 'image/png',
|
||||
href: 'http://example.com/attachment.png',
|
||||
},
|
||||
{
|
||||
type: 'Link',
|
||||
mediaType: 'image/avif',
|
||||
href: 'http://example.com/attachment.avif',
|
||||
},
|
||||
],
|
||||
}.deep_stringify_keys
|
||||
end
|
||||
|
||||
it 'returns the first option' do
|
||||
expect(subject).to have_attributes(
|
||||
remote_url: 'http://example.com/attachment.png',
|
||||
file_content_type: 'image/png'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -77,4 +77,24 @@ describe SearchQueryTransformer do
|
|||
expect(subject.send(:filter_clauses).map(&:term)).to contain_exactly(lt: '2022-01-01 23:00', time_zone: 'UTC')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple prefix clauses before a search term' do
|
||||
let(:query) { 'from:me has:media foo' }
|
||||
|
||||
it 'transforms clauses' do
|
||||
expect(subject.send(:must_clauses).map(&:term)).to contain_exactly('foo')
|
||||
expect(subject.send(:must_not_clauses)).to be_empty
|
||||
expect(subject.send(:filter_clauses).map(&:prefix)).to contain_exactly('from', 'has')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a search term between two prefix clauses' do
|
||||
let(:query) { 'from:me foo has:media' }
|
||||
|
||||
it 'transforms clauses' do
|
||||
expect(subject.send(:must_clauses).map(&:term)).to contain_exactly('foo')
|
||||
expect(subject.send(:must_not_clauses)).to be_empty
|
||||
expect(subject.send(:filter_clauses).map(&:prefix)).to contain_exactly('from', 'has')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
|||
type: 'Collection',
|
||||
id: actor.featured_collection_url,
|
||||
items: items,
|
||||
}.with_indifferent_access
|
||||
}.deep_stringify_keys
|
||||
end
|
||||
|
||||
shared_examples 'sets pinned posts' do
|
||||
|
|
@ -91,6 +91,55 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
|||
end
|
||||
|
||||
describe '#call' do
|
||||
subject { described_class.new.call(actor, note: true, hashtag: false) }
|
||||
|
||||
shared_examples 'sets pinned posts' do
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known), headers: { 'Content-Type': 'application/activity+json' })
|
||||
stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined), headers: { 'Content-Type': 'application/activity+json' })
|
||||
stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404)
|
||||
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' })
|
||||
stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: Oj.dump(featured_with_null), headers: { 'Content-Type': 'application/activity+json' })
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'sets expected posts as pinned posts' do
|
||||
expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly(
|
||||
'https://example.com/account/pinned/known',
|
||||
'https://example.com/account/pinned/unknown-inlined',
|
||||
'https://example.com/account/pinned/unknown-reachable'
|
||||
)
|
||||
expect(actor.pinned_statuses).to_not include(known_status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing the collection via an argument' do
|
||||
subject { described_class.new.call(actor, note: true, hashtag: false, collection: collection_or_uri) }
|
||||
|
||||
context 'when the collection is an URL' do
|
||||
let(:collection_or_uri) { actor.featured_collection_url }
|
||||
|
||||
before do
|
||||
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||
end
|
||||
|
||||
it_behaves_like 'sets pinned posts'
|
||||
end
|
||||
|
||||
context 'when the collection is inlined' do
|
||||
let(:collection_or_uri) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'Collection',
|
||||
items: items,
|
||||
}.deep_stringify_keys
|
||||
end
|
||||
|
||||
it_behaves_like 'sets pinned posts'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the endpoint is a Collection' do
|
||||
before do
|
||||
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||
|
|
@ -120,7 +169,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
|||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' })
|
||||
subject.call(actor, note: true, hashtag: false)
|
||||
subject
|
||||
end
|
||||
|
||||
it 'sets expected posts as pinned posts' do
|
||||
|
|
@ -156,7 +205,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
|||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' })
|
||||
subject.call(actor, note: true, hashtag: false)
|
||||
subject
|
||||
end
|
||||
|
||||
it 'sets expected posts as pinned posts' do
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue