Merge commit '1ee457f2d37c2b77a7fbce246a7c72ac9f9d3056' into glitch-soc/merge-upstream
This commit is contained in:
commit
291f141a02
@ -352,7 +352,7 @@ GEM
|
|||||||
azure-blob (~> 0.5.2)
|
azure-blob (~> 0.5.2)
|
||||||
hashie (~> 5.0)
|
hashie (~> 5.0)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.19.1)
|
json (2.19.2)
|
||||||
json-canonicalization (1.0.0)
|
json-canonicalization (1.0.0)
|
||||||
json-jwt (1.17.0)
|
json-jwt (1.17.0)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
|
|||||||
@ -166,6 +166,38 @@ on('change', '#domain_block_severity', ({ target }) => {
|
|||||||
if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
|
if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onChangeInviteUsersPermission = (target: HTMLInputElement) => {
|
||||||
|
const inviteBypassApprovalCheckbox = document.querySelector<HTMLInputElement>(
|
||||||
|
'input#user_role_permissions_as_keys_invite_bypass_approval',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (inviteBypassApprovalCheckbox) {
|
||||||
|
inviteBypassApprovalCheckbox.disabled = !target.checked;
|
||||||
|
|
||||||
|
if (target.checked) {
|
||||||
|
inviteBypassApprovalCheckbox.parentElement?.classList.remove('disabled');
|
||||||
|
inviteBypassApprovalCheckbox.parentElement?.parentElement?.classList.remove(
|
||||||
|
'disabled',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
inviteBypassApprovalCheckbox.parentElement?.classList.add('disabled');
|
||||||
|
inviteBypassApprovalCheckbox.parentElement?.parentElement?.classList.add(
|
||||||
|
'disabled',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
on(
|
||||||
|
'change',
|
||||||
|
'input#user_role_permissions_as_keys_invite_users',
|
||||||
|
({ target }) => {
|
||||||
|
if (target instanceof HTMLInputElement) {
|
||||||
|
onChangeInviteUsersPermission(target);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
function onEnableBootstrapTimelineAccountsChange(target: HTMLInputElement) {
|
function onEnableBootstrapTimelineAccountsChange(target: HTMLInputElement) {
|
||||||
const bootstrapTimelineAccountsField =
|
const bootstrapTimelineAccountsField =
|
||||||
document.querySelector<HTMLInputElement>(
|
document.querySelector<HTMLInputElement>(
|
||||||
@ -291,6 +323,13 @@ ready(() => {
|
|||||||
);
|
);
|
||||||
if (registrationMode) onChangeRegistrationMode(registrationMode);
|
if (registrationMode) onChangeRegistrationMode(registrationMode);
|
||||||
|
|
||||||
|
const inviteUsersPermissionChecbkox =
|
||||||
|
document.querySelector<HTMLInputElement>(
|
||||||
|
'input#user_role_permissions_as_keys_invite_users',
|
||||||
|
);
|
||||||
|
if (inviteUsersPermissionChecbkox)
|
||||||
|
onChangeInviteUsersPermission(inviteUsersPermissionChecbkox);
|
||||||
|
|
||||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
const checkAllElement = document.querySelector<HTMLInputElement>(
|
||||||
'#batch_checkbox_all',
|
'#batch_checkbox_all',
|
||||||
);
|
);
|
||||||
|
|||||||
@ -158,24 +158,11 @@ class Status extends ImmutablePureComponent {
|
|||||||
newRepliesIds: [],
|
newRepliesIds: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
UNSAFE_componentWillMount () {
|
componentDidMount() {
|
||||||
this.props.dispatch(fetchStatus(this.props.params.statusId, { forceFetch: true }));
|
this.props.dispatch(fetchStatus(this.props.params.statusId, { forceFetch: true }));
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
attachFullscreenListener(this.onFullScreenChange);
|
attachFullscreenListener(this.onFullScreenChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
|
||||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
|
||||||
this.props.dispatch(fetchStatus(nextProps.params.statusId, { forceFetch: true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
|
|
||||||
this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToggleMediaVisibility = () => {
|
handleToggleMediaVisibility = () => {
|
||||||
this.setState({ showMedia: !this.state.showMedia });
|
this.setState({ showMedia: !this.state.showMedia });
|
||||||
};
|
};
|
||||||
@ -493,8 +480,8 @@ class Status extends ImmutablePureComponent {
|
|||||||
this.statusNode = c;
|
this.statusNode = c;
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { status, descendantsIds } = this.props;
|
const { status, descendantsIds, params } = this.props;
|
||||||
|
|
||||||
const isSameStatus = status && (prevProps.status?.get('id') === status.get('id'));
|
const isSameStatus = status && (prevProps.status?.get('id') === status.get('id'));
|
||||||
|
|
||||||
@ -506,6 +493,14 @@ class Status extends ImmutablePureComponent {
|
|||||||
this.setState({newRepliesIds});
|
this.setState({newRepliesIds});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.statusId && prevProps.params.statusId !== params.statusId) {
|
||||||
|
this.props.dispatch(fetchStatus(params.statusId, { forceFetch: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status && status.get('id') !== this.state.loadedStatusId) {
|
||||||
|
this.setState({ showMedia: defaultMediaVisibility(this.props.status), loadedStatusId: status.get('id') });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
|||||||
@ -506,6 +506,10 @@ code {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label.checkbox {
|
label.checkbox {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class Invite < ApplicationRecord
|
|||||||
COMMENT_SIZE_LIMIT = 420
|
COMMENT_SIZE_LIMIT = 420
|
||||||
ELIGIBLE_CODE_CHARACTERS = [*('a'..'z'), *('A'..'Z'), *('0'..'9')].freeze
|
ELIGIBLE_CODE_CHARACTERS = [*('a'..'z'), *('A'..'Z'), *('0'..'9')].freeze
|
||||||
HOMOGLYPHS = %w(0 1 I l O).freeze
|
HOMOGLYPHS = %w(0 1 I l O).freeze
|
||||||
VALID_CODE_CHARACTERS = ELIGIBLE_CODE_CHARACTERS - HOMOGLYPHS
|
VALID_CODE_CHARACTERS = (ELIGIBLE_CODE_CHARACTERS - HOMOGLYPHS).freeze
|
||||||
|
|
||||||
belongs_to :user, inverse_of: :invites
|
belongs_to :user, inverse_of: :invites
|
||||||
has_many :users, inverse_of: :invite, dependent: nil
|
has_many :users, inverse_of: :invite, dependent: nil
|
||||||
@ -37,6 +37,10 @@ class Invite < ApplicationRecord
|
|||||||
(max_uses.nil? || uses < max_uses) && !expired? && user&.functional?
|
(max_uses.nil? || uses < max_uses) && !expired? && user&.functional?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bypass_approval?
|
||||||
|
user&.role&.can?(:invite_bypass_approval)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_code
|
def set_code
|
||||||
|
|||||||
@ -168,6 +168,10 @@ class User < ApplicationRecord
|
|||||||
invite_id.present? && invite.valid_for_use?
|
invite_id.present? && invite.valid_for_use?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def valid_bypassing_invitation?
|
||||||
|
valid_invitation? && invite.bypass_approval?
|
||||||
|
end
|
||||||
|
|
||||||
def disable!
|
def disable!
|
||||||
update!(disabled: true)
|
update!(disabled: true)
|
||||||
|
|
||||||
@ -420,7 +424,7 @@ class User < ApplicationRecord
|
|||||||
if requires_approval?
|
if requires_approval?
|
||||||
false
|
false
|
||||||
else
|
else
|
||||||
open_registrations? || valid_invitation? || external?
|
open_registrations? || valid_bypassing_invitation? || external?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -38,6 +38,7 @@ class UserRole < ApplicationRecord
|
|||||||
manage_user_access: (1 << 18),
|
manage_user_access: (1 << 18),
|
||||||
delete_user_data: (1 << 19),
|
delete_user_data: (1 << 19),
|
||||||
view_feeds: (1 << 20),
|
view_feeds: (1 << 20),
|
||||||
|
invite_bypass_approval: (1 << 21),
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
EVERYONE_ROLE_ID = -99
|
EVERYONE_ROLE_ID = -99
|
||||||
@ -51,10 +52,12 @@ class UserRole < ApplicationRecord
|
|||||||
ALL = FLAGS.values.reduce(&:|)
|
ALL = FLAGS.values.reduce(&:|)
|
||||||
|
|
||||||
DEFAULT = FLAGS[:invite_users]
|
DEFAULT = FLAGS[:invite_users]
|
||||||
|
SAFE = FLAGS[:invite_users] | FLAGS[:invite_bypass_approval]
|
||||||
|
|
||||||
CATEGORIES = {
|
CATEGORIES = {
|
||||||
invites: %i(
|
invites: %i(
|
||||||
invite_users
|
invite_users
|
||||||
|
invite_bypass_approval
|
||||||
).freeze,
|
).freeze,
|
||||||
|
|
||||||
moderation: %i(
|
moderation: %i(
|
||||||
@ -206,6 +209,6 @@ class UserRole < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_dangerous_permissions
|
def validate_dangerous_permissions
|
||||||
errors.add(:permissions_as_keys, :dangerous) if everyone? && Flags::DEFAULT & permissions != permissions
|
errors.add(:permissions_as_keys, :dangerous) if everyone? && Flags::SAFE & permissions != permissions
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::FetchRemoteFeaturedCollectionService < BaseService
|
||||||
|
include JsonLdHelper
|
||||||
|
|
||||||
|
def call(uri, on_behalf_of = nil)
|
||||||
|
json = fetch_resource(uri, true, on_behalf_of)
|
||||||
|
|
||||||
|
return unless supported_context?(json)
|
||||||
|
return unless json['type'] == 'FeaturedCollection'
|
||||||
|
|
||||||
|
# Fetching an unknown account should eventually also fetch its
|
||||||
|
# collections, so it should be OK to only handle known accounts here
|
||||||
|
account = Account.find_by(uri: json['attributedTo'])
|
||||||
|
return unless account
|
||||||
|
|
||||||
|
existing_collection = account.collections.find_by(uri:)
|
||||||
|
return existing_collection if existing_collection.present?
|
||||||
|
|
||||||
|
ActivityPub::ProcessFeaturedCollectionService.new.call(account, json)
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -27,7 +27,7 @@ class ActivityPub::ProcessFeaturedCollectionService
|
|||||||
tag_name: @json.dig('topic', 'name')
|
tag_name: @json.dig('topic', 'name')
|
||||||
)
|
)
|
||||||
|
|
||||||
process_items!
|
process_items! if @json['totalItems'].positive?
|
||||||
|
|
||||||
@collection
|
@collection
|
||||||
end
|
end
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class BackupService < BaseService
|
|||||||
skeleton = serialize(collection_presenter(STREAM_OUTBOX, size: account.statuses.count), ActivityPub::CollectionSerializer)
|
skeleton = serialize(collection_presenter(STREAM_OUTBOX, size: account.statuses.count), ActivityPub::CollectionSerializer)
|
||||||
skeleton[:@context] = full_context
|
skeleton[:@context] = full_context
|
||||||
skeleton[:orderedItems] = [PLACEHOLDER]
|
skeleton[:orderedItems] = [PLACEHOLDER]
|
||||||
skeleton = JSON.generate(skeleton)
|
skeleton = skeleton.to_json
|
||||||
prepend, append = skeleton.split(PLACEHOLDER.to_json)
|
prepend, append = skeleton.split(PLACEHOLDER.to_json)
|
||||||
|
|
||||||
file.write(prepend)
|
file.write(prepend)
|
||||||
@ -48,7 +48,7 @@ class BackupService < BaseService
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
JSON.generate(item)
|
item.to_json
|
||||||
end.join(','))
|
end.join(','))
|
||||||
|
|
||||||
GC.start
|
GC.start
|
||||||
@ -121,10 +121,8 @@ class BackupService < BaseService
|
|||||||
download_to_zip(zipfile, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists?
|
download_to_zip(zipfile, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists?
|
||||||
download_to_zip(zipfile, account.header, "header#{File.extname(account.header.path)}") if account.header.exists?
|
download_to_zip(zipfile, account.header, "header#{File.extname(account.header.path)}") if account.header.exists?
|
||||||
|
|
||||||
json = JSON.generate(actor)
|
|
||||||
|
|
||||||
zipfile.get_output_stream(STREAM_ACTOR) do |io|
|
zipfile.get_output_stream(STREAM_ACTOR) do |io|
|
||||||
io.write(json)
|
io.write(actor.to_json)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -133,7 +131,7 @@ class BackupService < BaseService
|
|||||||
|
|
||||||
skeleton.delete(:totalItems)
|
skeleton.delete(:totalItems)
|
||||||
skeleton[:orderedItems] = [PLACEHOLDER]
|
skeleton[:orderedItems] = [PLACEHOLDER]
|
||||||
skeleton = JSON.generate(skeleton)
|
skeleton = skeleton.to_json
|
||||||
prepend, append = skeleton.split(PLACEHOLDER.to_json)
|
prepend, append = skeleton.split(PLACEHOLDER.to_json)
|
||||||
|
|
||||||
zipfile.get_output_stream(STREAM_LIKES) do |io|
|
zipfile.get_output_stream(STREAM_LIKES) do |io|
|
||||||
@ -143,7 +141,7 @@ class BackupService < BaseService
|
|||||||
io.write(',') unless batch.zero?
|
io.write(',') unless batch.zero?
|
||||||
|
|
||||||
io.write(statuses.map do |status|
|
io.write(statuses.map do |status|
|
||||||
JSON.generate(ActivityPub::TagManager.instance.uri_for(status))
|
ActivityPub::TagManager.instance.uri_for(status).to_json
|
||||||
end.join(','))
|
end.join(','))
|
||||||
|
|
||||||
GC.start
|
GC.start
|
||||||
@ -161,7 +159,7 @@ class BackupService < BaseService
|
|||||||
skeleton = serialize(collection_presenter(STREAM_BOOKMARKS), ActivityPub::CollectionSerializer)
|
skeleton = serialize(collection_presenter(STREAM_BOOKMARKS), ActivityPub::CollectionSerializer)
|
||||||
skeleton.delete(:totalItems)
|
skeleton.delete(:totalItems)
|
||||||
skeleton[:orderedItems] = [PLACEHOLDER]
|
skeleton[:orderedItems] = [PLACEHOLDER]
|
||||||
skeleton = JSON.generate(skeleton)
|
skeleton = skeleton.to_json
|
||||||
prepend, append = skeleton.split(PLACEHOLDER.to_json)
|
prepend, append = skeleton.split(PLACEHOLDER.to_json)
|
||||||
|
|
||||||
zipfile.get_output_stream(STREAM_BOOKMARKS) do |io|
|
zipfile.get_output_stream(STREAM_BOOKMARKS) do |io|
|
||||||
@ -171,7 +169,7 @@ class BackupService < BaseService
|
|||||||
io.write(',') unless batch.zero?
|
io.write(',') unless batch.zero?
|
||||||
|
|
||||||
io.write(statuses.map do |status|
|
io.write(statuses.map do |status|
|
||||||
JSON.generate(ActivityPub::TagManager.instance.uri_for(status))
|
ActivityPub::TagManager.instance.uri_for(status).to_json
|
||||||
end.join(','))
|
end.join(','))
|
||||||
|
|
||||||
GC.start
|
GC.start
|
||||||
|
|||||||
@ -778,6 +778,8 @@ en:
|
|||||||
administrator_description: Users with this permission will bypass every permission
|
administrator_description: Users with this permission will bypass every permission
|
||||||
delete_user_data: Delete User Data
|
delete_user_data: Delete User Data
|
||||||
delete_user_data_description: Allows users to delete other users' data without delay
|
delete_user_data_description: Allows users to delete other users' data without delay
|
||||||
|
invite_bypass_approval: Invite Users without review
|
||||||
|
invite_bypass_approval_description: Allows people invited to the server by these users to bypass moderation approval
|
||||||
invite_users: Invite Users
|
invite_users: Invite Users
|
||||||
invite_users_description: Allows users to invite new people to the server
|
invite_users_description: Allows users to invite new people to the server
|
||||||
manage_announcements: Manage Announcements
|
manage_announcements: Manage Announcements
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddInviteApprovalBypassPermission < ActiveRecord::Migration[8.1]
|
||||||
|
class UserRole < ApplicationRecord; end
|
||||||
|
|
||||||
|
def up
|
||||||
|
UserRole.where('permissions & (1 << 16) = 1 << 16').update_all('permissions = permissions | (1 << 21)')
|
||||||
|
end
|
||||||
|
|
||||||
|
def down; end
|
||||||
|
end
|
||||||
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.1].define(version: 2026_03_11_152331) do
|
ActiveRecord::Schema[8.1].define(version: 2026_03_18_144837) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
|
||||||
|
|||||||
@ -298,7 +298,6 @@ RSpec.describe Auth::RegistrationsController do
|
|||||||
|
|
||||||
context 'with Approval-based registrations with valid invite and required invite text' do
|
context 'with Approval-based registrations with valid invite and required invite text' do
|
||||||
subject do
|
subject do
|
||||||
inviter = Fabricate(:user, confirmed_at: 2.days.ago)
|
|
||||||
Setting.registrations_mode = 'approved'
|
Setting.registrations_mode = 'approved'
|
||||||
Setting.require_invite_text = true
|
Setting.require_invite_text = true
|
||||||
request.headers['Accept-Language'] = accept_language
|
request.headers['Accept-Language'] = accept_language
|
||||||
@ -306,7 +305,9 @@ RSpec.describe Auth::RegistrationsController do
|
|||||||
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', invite_code: invite.code, agreement: 'true' } }
|
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', invite_code: invite.code, agreement: 'true' } }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redirects to setup and creates user' do
|
let!(:inviter) { Fabricate(:user, confirmed_at: 2.days.ago) }
|
||||||
|
|
||||||
|
it 'redirects to setup and creates user in a non-approved state' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
expect(response).to redirect_to auth_setup_path
|
expect(response).to redirect_to auth_setup_path
|
||||||
@ -315,9 +316,28 @@ RSpec.describe Auth::RegistrationsController do
|
|||||||
.to be_present
|
.to be_present
|
||||||
.and have_attributes(
|
.and have_attributes(
|
||||||
locale: eq(accept_language),
|
locale: eq(accept_language),
|
||||||
approved: be(true)
|
approved: be(false)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the inviting user has the permission to bypass approval' do
|
||||||
|
before do
|
||||||
|
inviter.role.update!(permissions: inviter.role.permissions | UserRole::FLAGS[:invite_bypass_approval])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to setup and creates user in an approved state' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to redirect_to auth_setup_path
|
||||||
|
|
||||||
|
expect(User.find_by(email: 'test@example.com'))
|
||||||
|
.to be_present
|
||||||
|
.and have_attributes(
|
||||||
|
locale: eq(accept_language),
|
||||||
|
approved: be(true)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with an already taken username' do
|
context 'with an already taken username' do
|
||||||
|
|||||||
@ -0,0 +1,64 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ActivityPub::FetchRemoteFeaturedCollectionService do
|
||||||
|
subject { described_class.new }
|
||||||
|
|
||||||
|
let(:account) { Fabricate(:remote_account) }
|
||||||
|
let(:uri) { 'https://example.com/featured_collections/1' }
|
||||||
|
let(:status) { 200 }
|
||||||
|
let(:response) do
|
||||||
|
{
|
||||||
|
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id' => uri,
|
||||||
|
'type' => 'FeaturedCollection',
|
||||||
|
'name' => 'Incredible people',
|
||||||
|
'summary' => 'These are really amazing',
|
||||||
|
'attributedTo' => account.uri,
|
||||||
|
'sensitive' => false,
|
||||||
|
'discoverable' => true,
|
||||||
|
'totalItems' => 0,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, uri)
|
||||||
|
.to_return_json(
|
||||||
|
status: status,
|
||||||
|
body: response,
|
||||||
|
headers: { 'Content-Type' => 'application/activity+json' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when collection does not exist' do
|
||||||
|
it 'creates a new collection' do
|
||||||
|
collection = nil
|
||||||
|
expect { collection = subject.call(uri) }.to change(Collection, :count).by(1)
|
||||||
|
|
||||||
|
expect(collection.uri).to eq uri
|
||||||
|
expect(collection.name).to eq 'Incredible people'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when collection already exists' do
|
||||||
|
let!(:collection) do
|
||||||
|
Fabricate(:remote_collection, account:, uri:, name: 'temp')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the existing collection' do
|
||||||
|
expect do
|
||||||
|
expect(subject.call(uri)).to eq collection
|
||||||
|
end.to_not change(Collection, :count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the URI can not be fetched' do
|
||||||
|
let(:response) { nil }
|
||||||
|
let(:status) { 404 }
|
||||||
|
|
||||||
|
it 'returns `nil`' do
|
||||||
|
expect(subject.call(uri)).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
x
Reference in New Issue
Block a user