mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Merge pull request #3436 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 1d46558e8d
This commit is contained in:
@@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
|
||||
include UserTrackingConcern
|
||||
include SessionTrackingConcern
|
||||
include CacheConcern
|
||||
include ErrorResponses
|
||||
include PreloadingConcern
|
||||
include DomainControlHelper
|
||||
include DatabaseHelper
|
||||
@@ -23,21 +24,6 @@ class ApplicationController < ActionController::Base
|
||||
helper_method :limited_federation_mode?
|
||||
helper_method :skip_csrf_meta_tags?
|
||||
|
||||
rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
|
||||
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
||||
rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found
|
||||
rescue_from ActionController::UnknownFormat, with: :not_acceptable
|
||||
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_content
|
||||
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
|
||||
|
||||
rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error)
|
||||
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable
|
||||
|
||||
rescue_from Seahorse::Client::NetworkingError do |e|
|
||||
Rails.logger.warn "Storage server error: #{e}"
|
||||
service_unavailable
|
||||
end
|
||||
|
||||
before_action :check_self_destruct!
|
||||
|
||||
before_action :store_referrer, except: :raise_not_found, if: :devise_controller?
|
||||
@@ -118,42 +104,6 @@ class ApplicationController < ActionController::Base
|
||||
ActiveModel::Type::Boolean.new.cast(params[key])
|
||||
end
|
||||
|
||||
def forbidden
|
||||
respond_with_error(403)
|
||||
end
|
||||
|
||||
def not_found
|
||||
respond_with_error(404)
|
||||
end
|
||||
|
||||
def gone
|
||||
respond_with_error(410)
|
||||
end
|
||||
|
||||
def unprocessable_content
|
||||
respond_with_error(422)
|
||||
end
|
||||
|
||||
def not_acceptable
|
||||
respond_with_error(406)
|
||||
end
|
||||
|
||||
def bad_request
|
||||
respond_with_error(400)
|
||||
end
|
||||
|
||||
def internal_server_error
|
||||
respond_with_error(500)
|
||||
end
|
||||
|
||||
def service_unavailable
|
||||
respond_with_error(503)
|
||||
end
|
||||
|
||||
def too_many_requests
|
||||
respond_with_error(429)
|
||||
end
|
||||
|
||||
def single_user_mode?
|
||||
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.without_internal.exists?
|
||||
end
|
||||
@@ -178,13 +128,6 @@ class ApplicationController < ActionController::Base
|
||||
@current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present?
|
||||
end
|
||||
|
||||
def respond_with_error(code)
|
||||
respond_to do |format|
|
||||
format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] }
|
||||
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code }
|
||||
end
|
||||
end
|
||||
|
||||
def check_self_destruct!
|
||||
return unless self_destruct?
|
||||
|
||||
|
||||
68
app/controllers/concerns/error_responses.rb
Normal file
68
app/controllers/concerns/error_responses.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ErrorResponses
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_content
|
||||
rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
|
||||
rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found
|
||||
rescue_from ActionController::UnknownFormat, with: :not_acceptable
|
||||
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
||||
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable
|
||||
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
|
||||
rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error)
|
||||
|
||||
rescue_from Seahorse::Client::NetworkingError do |e|
|
||||
Rails.logger.warn "Storage server error: #{e}"
|
||||
service_unavailable
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def bad_request
|
||||
respond_with_error(400)
|
||||
end
|
||||
|
||||
def forbidden
|
||||
respond_with_error(403)
|
||||
end
|
||||
|
||||
def gone
|
||||
respond_with_error(410)
|
||||
end
|
||||
|
||||
def internal_server_error
|
||||
respond_with_error(500)
|
||||
end
|
||||
|
||||
def not_acceptable
|
||||
respond_with_error(406)
|
||||
end
|
||||
|
||||
def not_found
|
||||
respond_with_error(404)
|
||||
end
|
||||
|
||||
def service_unavailable
|
||||
respond_with_error(503)
|
||||
end
|
||||
|
||||
def too_many_requests
|
||||
respond_with_error(429)
|
||||
end
|
||||
|
||||
def unprocessable_content
|
||||
respond_with_error(422)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def respond_with_error(code)
|
||||
respond_to do |format|
|
||||
format.any { render "errors/#{code}", layout: 'error', formats: [:html], status: code }
|
||||
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,6 +12,9 @@ class ActivityPub::Activity::Add < ActivityPub::Activity
|
||||
else
|
||||
add_featured
|
||||
end
|
||||
else
|
||||
@collection = @account.collections.find_by(uri: @json['target'])
|
||||
add_collection_item if @collection && Mastodon::Feature.collections_federation_enabled?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,4 +33,8 @@ class ActivityPub::Activity::Add < ActivityPub::Activity
|
||||
|
||||
FeaturedTag.create!(account: @account, name: name) if name.present?
|
||||
end
|
||||
|
||||
def add_collection_item
|
||||
ActivityPub::ProcessFeaturedItemService.new.call(@collection, @object)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,7 +29,7 @@ class CollectionItem < ApplicationRecord
|
||||
|
||||
validates :position, numericality: { only_integer: true, greater_than: 0 }
|
||||
validates :activity_uri, presence: true, if: :local_item_with_remote_account?
|
||||
validates :approval_uri, absence: true, unless: :local?
|
||||
validates :approval_uri, presence: true, unless: -> { local? || account&.local? }
|
||||
validates :account, presence: true, if: :accepted?
|
||||
validates :object_uri, presence: true, if: -> { account.nil? }
|
||||
validates :uri, presence: true, if: :remote?
|
||||
|
||||
34
app/services/activitypub/process_featured_item_service.rb
Normal file
34
app/services/activitypub/process_featured_item_service.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::ProcessFeaturedItemService
|
||||
include JsonLdHelper
|
||||
include Lockable
|
||||
include Redisable
|
||||
|
||||
def call(collection, uri_or_object)
|
||||
item_json = uri_or_object.is_a?(String) ? fetch_resource(uri_or_object, true) : uri_or_object
|
||||
return if non_matching_uri_hosts?(collection.uri, item_json['id'])
|
||||
|
||||
with_redis_lock("collection_item:#{item_json['id']}") do
|
||||
return if collection.collection_items.exists?(uri: item_json['id'])
|
||||
|
||||
@collection_item = collection.collection_items.create!(
|
||||
uri: item_json['id'],
|
||||
object_uri: item_json['featuredObject'],
|
||||
approval_uri: item_json['featureAuthorization']
|
||||
)
|
||||
|
||||
verify_authorization!
|
||||
|
||||
@collection_item
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_authorization!
|
||||
ActivityPub::VerifyFeaturedItemService.new.call(@collection_item)
|
||||
rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||
ActivityPub::VerifyFeaturedItemWorker.perform_in(rand(30..600).seconds, @collection_item.id)
|
||||
end
|
||||
end
|
||||
34
app/services/activitypub/verify_featured_item_service.rb
Normal file
34
app/services/activitypub/verify_featured_item_service.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::VerifyFeaturedItemService
|
||||
include JsonLdHelper
|
||||
|
||||
def call(collection_item)
|
||||
@collection_item = collection_item
|
||||
@authorization = fetch_resource(@collection_item.approval_uri, true, raise_on_error: :temporary)
|
||||
|
||||
if @authorization.nil?
|
||||
@collection_item.update!(state: :rejected)
|
||||
return
|
||||
end
|
||||
|
||||
return if non_matching_uri_hosts?(@collection_item.approval_uri, @authorization['interactionTarget'])
|
||||
return unless matching_type? && matching_collection_uri?
|
||||
|
||||
account = Account.where(uri: @collection_item.object_uri).first
|
||||
account ||= ActivityPub::FetchRemoteAccountService.new.call(@collection_item.object_uri)
|
||||
return if account.blank?
|
||||
|
||||
@collection_item.update!(account:, state: :accepted)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def matching_type?
|
||||
supported_context?(@authorization) && equals_or_includes?(@authorization['type'], 'FeatureAuthorization')
|
||||
end
|
||||
|
||||
def matching_collection_uri?
|
||||
@collection_item.collection.uri == @authorization['interactingObject']
|
||||
end
|
||||
end
|
||||
20
app/workers/activitypub/verify_featured_item_worker.rb
Normal file
20
app/workers/activitypub/verify_featured_item_worker.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::VerifyFeaturedItemWorker
|
||||
include Sidekiq::Worker
|
||||
include ExponentialBackoff
|
||||
include JsonLdHelper
|
||||
|
||||
sidekiq_options queue: 'pull', retry: 5
|
||||
|
||||
def perform(collection_item_id)
|
||||
collection_item = CollectionItem.find(collection_item_id)
|
||||
|
||||
ActivityPub::VerifyFeaturedItemService.new.call(collection_item)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
# Do nothing
|
||||
nil
|
||||
rescue Mastodon::UnexpectedResponseError => e
|
||||
raise e unless response_error_unsalvageable?(e.response)
|
||||
end
|
||||
end
|
||||
@@ -6,52 +6,21 @@ RSpec.describe ApplicationController do
|
||||
render_views
|
||||
|
||||
controller do
|
||||
def success
|
||||
head 200
|
||||
end
|
||||
|
||||
def routing_error
|
||||
raise ActionController::RoutingError, ''
|
||||
end
|
||||
|
||||
def record_not_found
|
||||
raise ActiveRecord::RecordNotFound, ''
|
||||
end
|
||||
|
||||
def invalid_authenticity_token
|
||||
raise ActionController::InvalidAuthenticityToken, ''
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'error response' do |code|
|
||||
it "returns http #{code} for http and renders template" do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(code)
|
||||
expect(response.parsed_body)
|
||||
.to have_css('body[class=error]')
|
||||
expect(response.parsed_body.css('h1').to_s)
|
||||
.to include(error_content(code))
|
||||
end
|
||||
|
||||
def error_content(code)
|
||||
if code == 422
|
||||
I18n.t('errors.422.content')
|
||||
else
|
||||
I18n.t("errors.#{code}")
|
||||
end
|
||||
end
|
||||
def success = head(200)
|
||||
end
|
||||
|
||||
context 'with a forgery' do
|
||||
subject do
|
||||
before do
|
||||
ActionController::Base.allow_forgery_protection = true
|
||||
routes.draw { post 'success' => 'anonymous#success' }
|
||||
post 'success'
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 422
|
||||
it 'responds with 422 and error page' do
|
||||
post 'success'
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(422)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'helper_method :current_account' do
|
||||
@@ -124,33 +93,6 @@ RSpec.describe ApplicationController do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ActionController::RoutingError' do
|
||||
subject do
|
||||
routes.draw { get 'routing_error' => 'anonymous#routing_error' }
|
||||
get 'routing_error'
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 404
|
||||
end
|
||||
|
||||
context 'with ActiveRecord::RecordNotFound' do
|
||||
subject do
|
||||
routes.draw { get 'record_not_found' => 'anonymous#record_not_found' }
|
||||
get 'record_not_found'
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 404
|
||||
end
|
||||
|
||||
context 'with ActionController::InvalidAuthenticityToken' do
|
||||
subject do
|
||||
routes.draw { get 'invalid_authenticity_token' => 'anonymous#invalid_authenticity_token' }
|
||||
get 'invalid_authenticity_token'
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 422
|
||||
end
|
||||
|
||||
describe 'before_action :check_suspension' do
|
||||
before do
|
||||
routes.draw { get 'success' => 'anonymous#success' }
|
||||
@@ -180,64 +122,4 @@ RSpec.describe ApplicationController do
|
||||
expect { controller.raise_not_found }.to raise_error(ActionController::RoutingError, 'No route matches unmatched')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'forbidden' do
|
||||
controller do
|
||||
def route_forbidden
|
||||
forbidden
|
||||
end
|
||||
end
|
||||
|
||||
subject do
|
||||
routes.draw { get 'route_forbidden' => 'anonymous#route_forbidden' }
|
||||
get 'route_forbidden'
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 403
|
||||
end
|
||||
|
||||
describe 'not_found' do
|
||||
controller do
|
||||
def route_not_found
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
subject do
|
||||
routes.draw { get 'route_not_found' => 'anonymous#route_not_found' }
|
||||
get 'route_not_found'
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 404
|
||||
end
|
||||
|
||||
describe 'gone' do
|
||||
controller do
|
||||
def route_gone
|
||||
gone
|
||||
end
|
||||
end
|
||||
|
||||
subject do
|
||||
routes.draw { get 'route_gone' => 'anonymous#route_gone' }
|
||||
get 'route_gone'
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 410
|
||||
end
|
||||
|
||||
describe 'unprocessable_content' do
|
||||
controller do
|
||||
def route_unprocessable_content
|
||||
unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
subject do
|
||||
routes.draw { get 'route_unprocessable_content' => 'anonymous#route_unprocessable_content' }
|
||||
get 'route_unprocessable_content'
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 422
|
||||
end
|
||||
end
|
||||
|
||||
128
spec/controllers/concerns/error_responses_spec.rb
Normal file
128
spec/controllers/concerns/error_responses_spec.rb
Normal file
@@ -0,0 +1,128 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ErrorResponses do
|
||||
render_views
|
||||
|
||||
shared_examples 'error response' do |code|
|
||||
before { routes.draw { get 'show' => 'anonymous#show' } }
|
||||
|
||||
it "returns http #{code} and renders error template" do
|
||||
get 'show'
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(code)
|
||||
expect(response.parsed_body)
|
||||
.to have_css('body[class=error]')
|
||||
.and have_css('h1', text: error_content(code))
|
||||
end
|
||||
|
||||
def error_content(code)
|
||||
I18n.t("errors.#{code}")
|
||||
.then { |value| I18n.t("errors.#{code}.content") if value.is_a?(Hash) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'bad_request' do
|
||||
controller(ApplicationController) do
|
||||
def show = bad_request
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 400
|
||||
end
|
||||
|
||||
describe 'forbidden' do
|
||||
controller(ApplicationController) do
|
||||
def show = forbidden
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 403
|
||||
end
|
||||
|
||||
describe 'gone' do
|
||||
controller(ApplicationController) do
|
||||
def show = gone
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 410
|
||||
end
|
||||
|
||||
describe 'internal_server_error' do
|
||||
controller(ApplicationController) do
|
||||
def show = internal_server_error
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 500
|
||||
end
|
||||
|
||||
describe 'not_acceptable' do
|
||||
controller(ApplicationController) do
|
||||
def show = not_acceptable
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 406
|
||||
end
|
||||
|
||||
describe 'not_found' do
|
||||
controller(ApplicationController) do
|
||||
def show = not_found
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 404
|
||||
end
|
||||
|
||||
describe 'service_unavailable' do
|
||||
controller(ApplicationController) do
|
||||
def show = service_unavailable
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 503
|
||||
end
|
||||
|
||||
describe 'too_many_requests' do
|
||||
controller(ApplicationController) do
|
||||
def show = too_many_requests
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 429
|
||||
end
|
||||
|
||||
describe 'unprocessable_content' do
|
||||
controller(ApplicationController) do
|
||||
def show = unprocessable_content
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 422
|
||||
end
|
||||
|
||||
context 'with ActionController::RoutingError' do
|
||||
controller(ApplicationController) do
|
||||
def show
|
||||
raise ActionController::RoutingError, ''
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 404
|
||||
end
|
||||
|
||||
context 'with ActiveRecord::RecordNotFound' do
|
||||
controller(ApplicationController) do
|
||||
def show
|
||||
raise ActiveRecord::RecordNotFound, ''
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 404
|
||||
end
|
||||
|
||||
context 'with ActionController::InvalidAuthenticityToken' do
|
||||
controller(ApplicationController) do
|
||||
def show
|
||||
raise ActionController::InvalidAuthenticityToken, ''
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', 422
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,7 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Add do
|
||||
context 'when the target is the featured collection' do
|
||||
let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured', domain: 'example.com') }
|
||||
let(:status) { Fabricate(:status, account: sender, visibility: :private) }
|
||||
|
||||
@@ -78,3 +79,42 @@ RSpec.describe ActivityPub::Activity::Add do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the target is a collection', feature: :collections_federation do
|
||||
subject { described_class.new(activity_json, collection.account) }
|
||||
|
||||
let(:collection) { Fabricate(:remote_collection) }
|
||||
let(:featured_item_json) do
|
||||
{
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => 'https://other.example.com/featured_item/1',
|
||||
'type' => 'FeaturedItem',
|
||||
'featuredObject' => 'https://example.com/actor/1',
|
||||
'featuredObjectType' => 'Person',
|
||||
'featureAuthorization' => 'https://example.com/auth/1',
|
||||
}
|
||||
end
|
||||
let(:activity_json) do
|
||||
{
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Add',
|
||||
'actor' => collection.account.uri,
|
||||
'target' => collection.uri,
|
||||
'object' => featured_item_json,
|
||||
}
|
||||
end
|
||||
let(:stubbed_service) do
|
||||
instance_double(ActivityPub::ProcessFeaturedItemService, call: true)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(ActivityPub::ProcessFeaturedItemService).to receive(:new).and_return(stubbed_service)
|
||||
end
|
||||
|
||||
it 'determines the correct collection and calls the service' do
|
||||
subject.perform
|
||||
|
||||
expect(stubbed_service).to have_received(:call).with(collection, featured_item_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,9 +21,13 @@ RSpec.describe CollectionItem do
|
||||
|
||||
let(:remote_collection) { Fabricate.build(:collection, local: false) }
|
||||
|
||||
it { is_expected.to validate_absence_of(:approval_uri) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:uri) }
|
||||
|
||||
context 'when account is not present' do
|
||||
subject { Fabricate.build(:collection_item, collection: remote_collection, account: nil) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:approval_uri) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is not present' do
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::ProcessFeaturedItemService do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:collection) { Fabricate(:remote_collection, uri: 'https://other.example.com/collection/1') }
|
||||
let(:featured_item_json) do
|
||||
{
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => 'https://other.example.com/featured_item/1',
|
||||
'type' => 'FeaturedItem',
|
||||
'featuredObject' => 'https://example.com/actor/1',
|
||||
'featuredObjectType' => 'Person',
|
||||
'featureAuthorization' => 'https://example.com/auth/1',
|
||||
}
|
||||
end
|
||||
let(:stubbed_service) do
|
||||
instance_double(ActivityPub::VerifyFeaturedItemService, call: true)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(ActivityPub::VerifyFeaturedItemService).to receive(:new).and_return(stubbed_service)
|
||||
end
|
||||
|
||||
shared_examples 'non-matching URIs' do
|
||||
context "when the item's URI does not match the collection's" do
|
||||
let(:collection) { Fabricate(:remote_collection) }
|
||||
|
||||
it 'does not create a collection item and returns `nil`' do
|
||||
expect do
|
||||
expect(subject.call(collection, object)).to be_nil
|
||||
end.to_not change(CollectionItem, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the collection item is inlined' do
|
||||
let(:object) { featured_item_json }
|
||||
|
||||
it_behaves_like 'non-matching URIs'
|
||||
|
||||
it 'creates and verifies the item' do
|
||||
expect { subject.call(collection, object) }.to change(collection.collection_items, :count).by(1)
|
||||
|
||||
expect(stubbed_service).to have_received(:call)
|
||||
|
||||
new_item = collection.collection_items.last
|
||||
expect(new_item.object_uri).to eq 'https://example.com/actor/1'
|
||||
expect(new_item.approval_uri).to eq 'https://example.com/auth/1'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only the id of the collection item is given' do
|
||||
let(:object) { featured_item_json['id'] }
|
||||
let(:featured_item_request) do
|
||||
stub_request(:get, object)
|
||||
.to_return_json(
|
||||
status: 200,
|
||||
body: featured_item_json,
|
||||
headers: { 'Content-Type' => 'application/activity+json' }
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
featured_item_request
|
||||
end
|
||||
|
||||
it_behaves_like 'non-matching URIs'
|
||||
|
||||
it 'fetches the collection item' do
|
||||
expect { subject.call(collection, object) }.to change(collection.collection_items, :count).by(1)
|
||||
|
||||
expect(featured_item_request).to have_been_requested
|
||||
|
||||
new_item = collection.collection_items.last
|
||||
expect(new_item.object_uri).to eq 'https://example.com/actor/1'
|
||||
expect(new_item.approval_uri).to eq 'https://example.com/auth/1'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,86 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::VerifyFeaturedItemService do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:collection) { Fabricate(:remote_collection) }
|
||||
let(:collection_item) do
|
||||
Fabricate(:collection_item,
|
||||
collection:,
|
||||
account: nil,
|
||||
state: :pending,
|
||||
uri: 'https://other.example.com/items/1',
|
||||
object_uri: 'https://example.com/actor/1',
|
||||
approval_uri: verification_json['id'])
|
||||
end
|
||||
let(:verification_json) do
|
||||
{
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'FeatureAuthorization',
|
||||
'id' => 'https://example.com/auth/1',
|
||||
'interactionTarget' => 'https://example.com/actor/1',
|
||||
'interactingObject' => collection.uri,
|
||||
}
|
||||
end
|
||||
let(:verification_request) do
|
||||
stub_request(:get, 'https://example.com/auth/1')
|
||||
.to_return_json(
|
||||
status: 200,
|
||||
body: verification_json,
|
||||
headers: { 'Content-Type' => 'application/activity+json' }
|
||||
)
|
||||
end
|
||||
let(:featured_account) { Fabricate(:remote_account, uri: 'https://example.com/actor/1') }
|
||||
|
||||
before { verification_request }
|
||||
|
||||
context 'when the authorization can be verified' do
|
||||
context 'when the featured account is known' do
|
||||
before { featured_account }
|
||||
|
||||
it 'verifies and creates the item' do
|
||||
subject.call(collection_item)
|
||||
|
||||
expect(verification_request).to have_been_requested
|
||||
|
||||
expect(collection_item.account_id).to eq featured_account.id
|
||||
expect(collection_item).to be_accepted
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the featured account is not known' do
|
||||
let(:stubbed_service) { instance_double(ActivityPub::FetchRemoteAccountService) }
|
||||
|
||||
before do
|
||||
allow(stubbed_service).to receive(:call).with('https://example.com/actor/1') { featured_account }
|
||||
allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(stubbed_service)
|
||||
end
|
||||
|
||||
it 'fetches the actor and creates the item' do
|
||||
subject.call(collection_item)
|
||||
|
||||
expect(stubbed_service).to have_received(:call)
|
||||
expect(verification_request).to have_been_requested
|
||||
|
||||
expect(collection_item.account_id).to eq featured_account.id
|
||||
expect(collection_item).to be_accepted
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the authorization cannot be verified' do
|
||||
let(:verification_request) do
|
||||
stub_request(:get, 'https://example.com/auth/1')
|
||||
.to_return(status: 404)
|
||||
end
|
||||
|
||||
it 'creates item without attached account and in proper state' do
|
||||
subject.call(collection_item)
|
||||
|
||||
expect(collection_item.account_id).to be_nil
|
||||
expect(collection_item).to be_rejected
|
||||
end
|
||||
end
|
||||
end
|
||||
32
spec/workers/activitypub/verify_featured_item_worker_spec.rb
Normal file
32
spec/workers/activitypub/verify_featured_item_worker_spec.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::VerifyFeaturedItemWorker do
|
||||
let(:worker) { described_class.new }
|
||||
let(:service) { instance_double(ActivityPub::VerifyFeaturedItemService, call: true) }
|
||||
|
||||
describe '#perform' do
|
||||
let(:collection_item) { Fabricate(:unverified_remote_collection_item) }
|
||||
|
||||
before { stub_service }
|
||||
|
||||
it 'sends the status to the service' do
|
||||
worker.perform(collection_item.id)
|
||||
|
||||
expect(service).to have_received(:call).with(collection_item)
|
||||
end
|
||||
|
||||
it 'returns nil for non-existent record' do
|
||||
result = worker.perform(123_123_123)
|
||||
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
def stub_service
|
||||
allow(ActivityPub::VerifyFeaturedItemService)
|
||||
.to receive(:new)
|
||||
.and_return(service)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user