Compare commits

...

10 Commits

Author SHA1 Message Date
hgw 0363c2043e
Add usual patches 2024-02-02 01:33:44 +00:00
Claire a6641f828b
Merge pull request from GHSA-3fjr-858r-92rw
* Fix insufficient origin validation

* Bump version to v4.2.5
2024-02-01 15:56:46 +01:00
Claire 4633bb8ce0 Bump version to v4.2.4 2024-01-24 15:31:13 +01:00
Claire 1ab050eb52 Change PostgreSQL version check to check for PostgreSQL 10+ 2024-01-24 15:31:13 +01:00
Claire 4eb98ef755 Ignore the devise-two-factor advisory as we have rate limits in place (#28733) 2024-01-24 15:31:13 +01:00
Claire 7a22999f92 Bump ruby version to 3.2.3 2024-01-24 15:31:13 +01:00
Claire c5c464804d Update dependency puma to v6.4.2 2024-01-24 15:31:13 +01:00
Claire 779237f054 Fix error when processing remote files with unusually long names (#28823) 2024-01-24 15:31:13 +01:00
Claire b377f82b1d Fix processing of compacted single-item JSON-LD collections (#28816) 2024-01-24 15:31:13 +01:00
Claire 6fe2a47357 Add rate-limit of TOTP authentication attempts at controller level (#28801) 2024-01-24 15:31:13 +01:00
39 changed files with 199 additions and 65 deletions

6
.bundler-audit.yml Normal file
View File

@ -0,0 +1,6 @@
---
ignore:
# devise-two-factor advisory about brute-forcing TOTP
# We have rate-limits on authentication endpoints in place (including second
# factor verification) since Mastodon v3.2.0
- CVE-2024-0227

View File

@ -1 +1 @@
3.2.2
3.2.3

View File

@ -2,6 +2,38 @@
All notable changes to this project will be documented in this file.
## [4.2.5] - 2024-02-01
### Security
- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw))
## [4.2.4] - 2024-01-24
### Fixed
- Fix error when processing remote files with unusually long names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28823))
- Fix processing of compacted single-item JSON-LD collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28816))
- Retry 401 errors on replies fetching ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28788))
- Fix `RecordNotUnique` errors in LinkCrawlWorker ([tribela](https://github.com/mastodon/mastodon/pull/28748))
- Fix Mastodon not correctly processing HTTP Signatures with query strings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28443), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28476))
- Fix potential redirection loop of streaming endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28665))
- Fix streaming API redirection ignoring the port of `streaming_api_base_url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28558))
- Fix error when processing link preview with an array as `inLanguage` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28252))
- Fix unsupported time zone or locale preventing sign-up ([Gargron](https://github.com/mastodon/mastodon/pull/28035))
- Fix "Hide these posts from home" list setting not refreshing when switching lists ([brianholley](https://github.com/mastodon/mastodon/pull/27763))
- Fix missing background behind dismissable banner in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/27479))
- Fix line wrapping of language selection button with long locale codes ([gunchleoc](https://github.com/mastodon/mastodon/pull/27100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27127))
- Fix `Undo Announce` activity not being sent to non-follower authors ([MitarashiDango](https://github.com/mastodon/mastodon/pull/18482))
- Fix N+1s because of association preloaders not actually getting called ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28339))
- Fix empty column explainer getting cropped under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28337))
- Fix `LinkCrawlWorker` error when encountering empty OEmbed response ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28268))
- Fix call to inefficient `delete_matched` cache method in domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28367))
### Security
- Add rate-limit of TOTP authentication attempts at controller level ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28801))
## [4.2.3] - 2023-12-05
### Fixed

View File

@ -2,7 +2,7 @@
# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim
ARG NODE_VERSION="20.6-bookworm-slim"
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.3-slim as ruby
FROM node:${NODE_VERSION} as build
COPY --link --from=ruby /opt/ruby /opt/ruby

View File

@ -479,7 +479,7 @@ GEM
net-smtp (0.3.3)
net-protocol
net-ssh (7.1.0)
nio4r (2.5.9)
nio4r (2.7.0)
nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
@ -534,7 +534,7 @@ GEM
premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0)
public_suffix (5.0.3)
puma (6.3.1)
puma (6.4.2)
nio4r (~> 2.0)
pundit (2.3.0)
activesupport (>= 3.0.0)

View File

@ -17,6 +17,4 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| ------- | ---------------- |
| 4.2.x | Yes |
| 4.1.x | Yes |
| 4.0.x | No |
| 3.5.x | Until 2023-12-31 |
| < 3.5 | No |
| < 4.1 | No |

View File

@ -1,6 +1,10 @@
# frozen_string_literal: true
class Auth::SessionsController < Devise::SessionsController
include Redisable
MAX_2FA_ATTEMPTS_PER_HOUR = 10
layout 'auth'
skip_before_action :require_no_authentication, only: [:create]
@ -134,9 +138,23 @@ class Auth::SessionsController < Devise::SessionsController
session.delete(:attempt_user_updated_at)
end
def clear_2fa_attempt_from_user(user)
redis.del(second_factor_attempts_key(user))
end
def check_second_factor_rate_limits(user)
attempts, = redis.multi do |multi|
multi.incr(second_factor_attempts_key(user))
multi.expire(second_factor_attempts_key(user), 1.hour)
end
attempts >= MAX_2FA_ATTEMPTS_PER_HOUR
end
def on_authentication_success(user, security_measure)
@on_authentication_success_called = true
clear_2fa_attempt_from_user(user)
clear_attempt_from_session
user.update_sign_in!(new_sign_in: true)
@ -168,4 +186,8 @@ class Auth::SessionsController < Devise::SessionsController
user_agent: request.user_agent
)
end
def second_factor_attempts_key(user)
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
end
end

View File

@ -266,7 +266,7 @@ module SignatureVerification
stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
account
end
rescue Mastodon::PrivateNetworkAddressError => e

View File

@ -65,6 +65,11 @@ module TwoFactorAuthenticationConcern
end
def authenticate_with_two_factor_via_otp(user)
if check_second_factor_rate_limits(user)
flash.now[:alert] = I18n.t('users.rate_limited')
return prompt_for_two_factor(user)
end
if valid_otp_attempt?(user)
on_authentication_success(user, :otp)
else

View File

@ -155,8 +155,8 @@ module JsonLdHelper
end
end
def fetch_resource(uri, id, on_behalf_of = nil, request_options: {})
unless id
def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {})
unless id_is_known
json = fetch_resource_without_id_validation(uri, on_behalf_of)
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])

View File

@ -100,7 +100,7 @@ class ComposeForm extends ImmutablePureComponent {
const fulltext = this.getFulltextForCharacterCounting();
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia));
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 2000 || (isOnlyWhitespace && !anyMedia));
};
handleSubmit = (e) => {
@ -297,7 +297,7 @@ class ComposeForm extends ImmutablePureComponent {
</div>
<div className='character-counter__wrapper'>
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />
<CharacterCounter max={2000} text={this.getFulltextForCharacterCounting()} />
</div>
</div>
</div>

View File

@ -154,7 +154,7 @@ class ActivityPub::Activity
if object_uri.start_with?('http')
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first, request_id: @options[:request_id])
ActivityPub::FetchRemoteStatusService.new.call(object_uri, on_behalf_of: @account.followers.local.first, request_id: @options[:request_id])
elsif @object['url'].present?
::FetchRemoteStatusService.new.call(@object['url'], request_id: @options[:request_id])
end

View File

@ -19,7 +19,7 @@ class ActivityPub::LinkedDataSignature
return unless type == 'RsaSignature2017'
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank?
creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri) if creator&.public_key.blank?
return if creator.nil?

View File

@ -125,6 +125,7 @@ class User < ApplicationRecord
before_validation :sanitize_time_zone
before_validation :sanitize_locale
before_create :set_approved
before_create :auto_confirm
after_commit :send_pending_devise_notifications
after_create_commit :trigger_webhooks
@ -427,6 +428,10 @@ class User < ApplicationRecord
end
end
def auto_confirm
self.confirmed_at = Time.now.utc
end
def sign_up_from_ip_requires_approval?
!sign_up_ip.nil? && IpBlock.where(severity: :sign_up_requires_approval).where('ip >>= ?', sign_up_ip.to_s).exists?
end

View File

@ -23,9 +23,9 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
case collection['type']
when 'Collection', 'CollectionPage'
collection['items']
as_array(collection['items'])
when 'OrderedCollection', 'OrderedCollectionPage'
collection['orderedItems']
as_array(collection['orderedItems'])
end
end

View File

@ -2,7 +2,7 @@
class ActivityPub::FetchRemoteAccountService < ActivityPub::FetchRemoteActorService
# Does a WebFinger roundtrip on each call, unless `only_key` is true
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
def call(uri, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
actor = super
return actor if actor.nil? || actor.is_a?(Account)

View File

@ -10,15 +10,15 @@ class ActivityPub::FetchRemoteActorService < BaseService
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
# Does a WebFinger roundtrip on each call, unless `only_key` is true
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
def call(uri, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
return if domain_not_allowed?(uri)
return ActivityPub::TagManager.instance.uri_to_actor(uri) if ActivityPub::TagManager.instance.local_uri?(uri)
@json = begin
if prefetched_body.nil?
fetch_resource(uri, id)
fetch_resource(uri, true)
else
body_to_json(prefetched_body, compare_id: id ? uri : nil)
body_to_json(prefetched_body, compare_id: uri)
end
rescue Oj::ParseError
raise Error, "Error parsing JSON-LD document #{uri}"

View File

@ -6,23 +6,10 @@ class ActivityPub::FetchRemoteKeyService < BaseService
class Error < StandardError; end
# Returns actor that owns the key
def call(uri, id: true, prefetched_body: nil, suppress_errors: true)
def call(uri, suppress_errors: true)
raise Error, 'No key URI given' if uri.blank?
if prefetched_body.nil?
if id
@json = fetch_resource_without_id_validation(uri)
if actor_type?
@json = fetch_resource(@json['id'], true)
elsif uri != @json['id']
raise Error, "Fetched URI #{uri} has wrong id #{@json['id']}"
end
else
@json = fetch_resource(uri, id)
end
else
@json = body_to_json(prefetched_body, compare_id: id ? uri : nil)
end
@json = fetch_resource(uri, false)
raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil?
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json)

View File

@ -8,14 +8,14 @@ class ActivityPub::FetchRemoteStatusService < BaseService
DISCOVERIES_PER_REQUEST = 1000
# Should be called when uri has already been checked for locality
def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
def call(uri, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
return if domain_not_allowed?(uri)
@request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
@json = if prefetched_body.nil?
fetch_resource(uri, id, on_behalf_of)
fetch_resource(uri, true, on_behalf_of)
else
body_to_json(prefetched_body, compare_id: id ? uri : nil)
body_to_json(prefetched_body, compare_id: uri)
end
return unless supported_context?
@ -65,7 +65,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
def account_from_uri(uri)
actor = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
actor = ActivityPub::FetchRemoteAccountService.new.call(uri, id: true, request_id: @request_id) if actor.nil? || actor.possibly_stale?
actor = ActivityPub::FetchRemoteAccountService.new.call(uri, request_id: @request_id) if actor.nil? || actor.possibly_stale?
actor
end

View File

@ -26,9 +26,9 @@ class ActivityPub::FetchRepliesService < BaseService
case collection['type']
when 'Collection', 'CollectionPage'
collection['items']
as_array(collection['items'])
when 'OrderedCollection', 'OrderedCollectionPage'
collection['orderedItems']
as_array(collection['orderedItems'])
end
end

View File

@ -277,7 +277,7 @@ class ActivityPub::ProcessAccountService < BaseService
def moved_account
account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account)
account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true, break_on_redirect: true, request_id: @options[:request_id])
account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], break_on_redirect: true, request_id: @options[:request_id])
account
end

View File

@ -59,9 +59,9 @@ class ActivityPub::SynchronizeFollowersService < BaseService
case collection['type']
when 'Collection', 'CollectionPage'
collection['items']
as_array(collection['items'])
when 'OrderedCollection', 'OrderedCollectionPage'
collection['orderedItems']
as_array(collection['orderedItems'])
end
end

View File

@ -48,7 +48,15 @@ class FetchResourceService < BaseService
body = response.body_with_limit
json = body_to_json(body)
[json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json))
return unless supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json))
if json['id'] != @url
return if terminal
return process(json['id'], terminal: true)
end
[@url, { prefetched_body: body }]
elsif !terminal
link_header = response['Link'] && parse_link_header(response)

View File

@ -69,7 +69,7 @@ class Keys::QueryService < BaseService
return if json['items'].blank?
@devices = json['items'].map do |device|
@devices = as_array(json['items']).map do |device|
Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim'])
end
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class StatusLengthValidator < ActiveModel::Validator
MAX_CHARS = 500
MAX_CHARS = 2000
URL_PLACEHOLDER_CHARS = 23
URL_PLACEHOLDER = 'x' * 23

View File

@ -1826,6 +1826,7 @@ en:
go_to_sso_account_settings: Go to your identity provider's account settings
invalid_otp_token: Invalid two-factor code
otp_lost_help_html: If you lost access to both, you may get in touch with %{email}
rate_limited: Too many authentication attempts, try again later.
seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available.
signed_in_as: 'Signed in as:'
verification:

View File

@ -56,7 +56,7 @@ services:
web:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.3
image: ghcr.io/mastodon/mastodon:v4.2.5
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.3
image: ghcr.io/mastodon/mastodon:v4.2.5
restart: always
env_file: .env.production
command: node ./streaming
@ -95,7 +95,7 @@ services:
sidekiq:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.3
image: ghcr.io/mastodon/mastodon:v4.2.5
restart: always
env_file: .env.production
command: bundle exec sidekiq

View File

@ -13,7 +13,7 @@ module Mastodon
end
def patch
3
5
end
def default_prerelease

View File

@ -16,7 +16,7 @@ module Paperclip
private
def cache_current_values
@original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data'
@original_filename = truncated_filename
@tempfile = copy_to_tempfile(@target)
@content_type = ContentTypeDetector.new(@tempfile.path).detect
@size = File.size(@tempfile)
@ -43,6 +43,13 @@ module Paperclip
source.response.connection.close
end
def truncated_filename
filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data'
extension = File.extname(filename)
basename = File.basename(filename, extension)
[basename[...20], extension[..4]].compact_blank.join
end
def filename_from_content_disposition
disposition = @target.response.headers['content-disposition']
disposition&.match(/filename="([^"]*)"/)&.captures&.first

View File

@ -17,7 +17,7 @@ namespace :db do
task :pre_migration_check do
version = ActiveRecord::Base.connection.select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
abort 'This version of Mastodon requires PostgreSQL 9.5 or newer. Please update PostgreSQL before updating Mastodon' if version < 90_500
abort 'This version of Mastodon requires PostgreSQL 10.0 or newer. Please update PostgreSQL before updating Mastodon' if version < 100_000
end
Rake::Task['db:migrate'].enhance(['db:pre_migration_check'])

View File

@ -263,6 +263,26 @@ RSpec.describe Auth::SessionsController do
end
end
context 'when repeatedly using an invalid TOTP code before using a valid code' do
before do
stub_const('Auth::SessionsController::MAX_2FA_ATTEMPTS_PER_HOUR', 2)
end
it 'does not log the user in' do
# Travel to the beginning of an hour to avoid crossing rate-limit buckets
travel_to '2023-12-20T10:00:00Z'
Auth::SessionsController::MAX_2FA_ATTEMPTS_PER_HOUR.times do
post :create, params: { user: { otp_attempt: '1234' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
expect(controller.current_user).to be_nil
end
post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
expect(controller.current_user).to be_nil
expect(flash[:alert]).to match I18n.t('users.rate_limited')
end
end
context 'when using a valid OTP' do
before do
post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }

View File

@ -60,7 +60,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub)
allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) do
allow(service_stub).to receive(:call).with('http://example.com/alice') do
sender.update!(public_key: old_key)
sender
end
@ -68,7 +68,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
it 'fetches key and returns creator' do
expect(subject.verify_actor!).to eq sender
expect(service_stub).to have_received(:call).with('http://example.com/alice', id: false).once
expect(service_stub).to have_received(:call).with('http://example.com/alice').once
end
end

View File

@ -31,7 +31,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
}
end
let(:status_json_pinned_unknown_unreachable) do
let(:status_json_pinned_unknown_reachable) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Note',
@ -65,7 +65,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known))
stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined))
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_unreachable))
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable))
subject.call(actor, note: true, hashtag: false)
end
@ -103,6 +103,21 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
end
it_behaves_like 'sets pinned posts'
context 'when there is a single item, with the array compacted away' do
let(:items) { 'https://example.com/account/pinned/unknown-reachable' }
before do
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable))
subject.call(actor, note: true, hashtag: false)
end
it 'sets expected posts as pinned posts' do
expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly(
'https://example.com/account/pinned/unknown-reachable'
)
end
end
end
context 'when the endpoint is a paginated Collection' do
@ -124,6 +139,21 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
end
it_behaves_like 'sets pinned posts'
context 'when there is a single item, with the array compacted away' do
let(:items) { 'https://example.com/account/pinned/unknown-reachable' }
before do
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable))
subject.call(actor, note: true, hashtag: false)
end
it 'sets expected posts as pinned posts' do
expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly(
'https://example.com/account/pinned/unknown-reachable'
)
end
end
end
end
end

View File

@ -18,7 +18,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
end
describe '#call' do
let(:account) { subject.call('https://example.com/alice', id: true) }
let(:account) { subject.call('https://example.com/alice') }
shared_examples 'sets profile data' do
it 'returns an account' do

View File

@ -18,7 +18,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
end
describe '#call' do
let(:account) { subject.call('https://example.com/alice', id: true) }
let(:account) { subject.call('https://example.com/alice') }
shared_examples 'sets profile data' do
it 'returns an account' do

View File

@ -55,7 +55,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do
end
describe '#call' do
let(:account) { subject.call(public_key_id, id: false) }
let(:account) { subject.call(public_key_id) }
context 'when the key is a sub-object from the actor' do
before do

View File

@ -34,6 +34,18 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do
describe '#call' do
context 'when the payload is a Collection with inlined replies' do
context 'when there is a single reply, with the array compacted away' do
let(:items) { 'http://example.com/self-reply-1' }
it 'queues the expected worker' do
allow(FetchReplyWorker).to receive(:push_bulk)
subject.call(status, payload)
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1'])
end
end
context 'when passing the collection itself' do
it 'spawns workers for up to 5 replies on the same server' do
expect(FetchReplyWorker).to receive(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])

View File

@ -57,7 +57,7 @@ RSpec.describe FetchResourceService, type: :service do
let(:json) do
{
id: 1,
id: 'http://example.com/foo',
'@context': ActivityPub::TagManager::CONTEXT,
type: 'Note',
}.to_json
@ -83,27 +83,27 @@ RSpec.describe FetchResourceService, type: :service do
let(:content_type) { 'application/activity+json; charset=utf-8' }
let(:body) { json }
it { is_expected.to eq [1, { prefetched_body: body, id: true }] }
it { is_expected.to eq ['http://example.com/foo', { prefetched_body: body }] }
end
context 'when content type is ld+json with profile' do
let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }
let(:body) { json }
it { is_expected.to eq [1, { prefetched_body: body, id: true }] }
it { is_expected.to eq ['http://example.com/foo', { prefetched_body: body }] }
end
context 'when link header is present' do
let(:headers) { { 'Link' => '<http://example.com/foo>; rel="alternate"; type="application/activity+json"' } }
it { is_expected.to eq [1, { prefetched_body: json, id: true }] }
it { is_expected.to eq ['http://example.com/foo', { prefetched_body: json }] }
end
context 'when content type is text/html' do
let(:content_type) { 'text/html' }
let(:body) { '<html><head><link rel="alternate" href="http://example.com/foo" type="application/activity+json"/></head></html>' }
it { is_expected.to eq [1, { prefetched_body: json, id: true }] }
it { is_expected.to eq ['http://example.com/foo', { prefetched_body: json }] }
end
end
end

View File

@ -139,6 +139,7 @@ describe ResolveURLService, type: :service do
stub_request(:get, url).to_return(status: 302, headers: { 'Location' => status_url })
body = ActiveModelSerializers::SerializableResource.new(status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter).to_json
stub_request(:get, status_url).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' })
stub_request(:get, uri).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' })
end
it 'returns status by url' do