Merge tag 'v4.3.0-rc.1'

This commit is contained in:
Mike Barnes 2024-10-02 10:34:27 +10:00
commit 26c9b9ba39
3459 changed files with 130932 additions and 69993 deletions

View file

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

View file

@ -50,7 +50,7 @@ RSpec.describe ActivityPub::Activity::Add do
end
it 'fetches the status and pins it' do
allow(service_stub).to receive(:call) do |uri, id: true, on_behalf_of: nil, request_id: nil| # rubocop:disable Lint/UnusedBlockArgument
allow(service_stub).to receive(:call) do |uri, id: true, on_behalf_of: nil, **|
expect(uri).to eq 'https://example.com/unknown'
expect(id).to be true
expect(on_behalf_of&.following?(sender)).to be true
@ -64,7 +64,7 @@ RSpec.describe ActivityPub::Activity::Add do
context 'when there is no local follower' do
it 'tries to fetch the status' do
allow(service_stub).to receive(:call) do |uri, id: true, on_behalf_of: nil, request_id: nil| # rubocop:disable Lint/UnusedBlockArgument
allow(service_stub).to receive(:call) do |uri, id: true, on_behalf_of: nil, **|
expect(uri).to eq 'https://example.com/unknown'
expect(id).to be true
expect(on_behalf_of).to be_nil

View file

@ -77,13 +77,6 @@ RSpec.describe ActivityPub::Activity::Create do
follower.follow!(sender)
end
around do |example|
Sidekiq::Testing.fake! do
example.run
Sidekiq::Worker.clear_all
end
end
it 'correctly processes posts and inserts them in timelines', :aggregate_failures do
# Simulate a temporary failure preventing from fetching the parent post
stub_request(:get, object_json[:id]).to_return(status: 500)
@ -539,15 +532,21 @@ RSpec.describe ActivityPub::Activity::Create do
mediaType: 'image/png',
url: 'http://example.com/attachment.png',
},
{
type: 'Document',
mediaType: 'image/png',
url: 'http://example.com/emoji.png',
},
],
}
end
it 'creates status' do
it 'creates status with correctly-ordered media attachments' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
expect(status.ordered_media_attachments.map(&:remote_url)).to eq ['http://example.com/attachment.png', 'http://example.com/emoji.png']
expect(status.ordered_media_attachment_ids).to be_present
end
end
@ -894,58 +893,46 @@ RSpec.describe ActivityPub::Activity::Create do
end
end
context 'with an encrypted message' do
subject { described_class.new(json, sender, delivery: true, delivered_to_account_id: recipient.id) }
context 'when object URI uses bearcaps' do
subject { described_class.new(json, sender) }
let(:token) { 'foo' }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
type: 'Create',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: Addressable::URI.new(scheme: 'bear', query_values: { t: token, u: object_json[:id] }).to_s,
}.with_indifferent_access
end
let(:recipient) { Fabricate(:account) }
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'EncryptedMessage',
attributedTo: {
type: 'Device',
deviceId: '1234',
},
to: {
type: 'Device',
deviceId: target_device.device_id,
},
messageType: 1,
cipherText: 'Foo',
messageFranking: 'Baz678',
digest: {
digestAlgorithm: 'Bar456',
digestValue: 'Foo123',
},
type: 'Note',
content: 'Lorem ipsum',
to: 'https://www.w3.org/ns/activitystreams#Public',
}
end
let(:target_device) { Fabricate(:device, account: recipient) }
before do
stub_request(:get, object_json[:id])
.with(headers: { Authorization: "Bearer #{token}" })
.to_return(body: Oj.dump(object_json), headers: { 'Content-Type': 'application/activity+json' })
subject.perform
end
it 'creates an encrypted message' do
encrypted_message = target_device.encrypted_messages.reload.first
it 'creates status' do
status = sender.statuses.first
expect(encrypted_message).to_not be_nil
expect(encrypted_message.from_device_id).to eq '1234'
expect(encrypted_message.from_account).to eq sender
expect(encrypted_message.type).to eq 1
expect(encrypted_message.body).to eq 'Foo'
expect(encrypted_message.digest).to eq 'Foo123'
end
it 'creates a message franking' do
encrypted_message = target_device.encrypted_messages.reload.first
message_franking = encrypted_message.message_franking
crypt = ActiveSupport::MessageEncryptor.new(SystemKey.current_key, serializer: Oj)
json = crypt.decrypt_and_verify(message_franking)
expect(json['source_account_id']).to eq sender.id
expect(json['target_account_id']).to eq recipient.id
expect(json['original_franking']).to eq 'Baz678'
expect(status).to_not be_nil
expect(status).to have_attributes(
visibility: 'public',
text: 'Lorem ipsum'
)
end
end

View file

@ -47,9 +47,13 @@ RSpec.describe ActivityPub::Activity::Delete do
expect(Status.find_by(id: status.id)).to be_nil
end
it 'sends delete activity to followers of rebloggers' do
it 'sends delete activity to followers of rebloggers', :inline_jobs do
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
end
it 'deletes the reblog' do
expect { reblog.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end

View file

@ -36,6 +36,7 @@ RSpec.describe ActivityPub::Activity::Flag do
expect(report).to_not be_nil
expect(report.comment).to eq 'Boo!!'
expect(report.status_ids).to eq [status.id]
expect(report.application).to be_nil
end
end
@ -54,7 +55,7 @@ RSpec.describe ActivityPub::Activity::Flag do
}.with_indifferent_access, sender)
end
let(:long_comment) { Faker::Lorem.characters(number: 6000) }
let(:long_comment) { 'a' * described_class::COMMENT_SIZE_LIMIT * 2 }
before do
subject.perform
@ -63,10 +64,12 @@ RSpec.describe ActivityPub::Activity::Flag do
it 'creates a report but with a truncated comment' do
report = Report.find_by(account: sender, target_account: flagged)
expect(report).to_not be_nil
expect(report.comment.length).to eq 5000
expect(report.comment).to eq long_comment[0...5000]
expect(report.status_ids).to eq [status.id]
expect(report)
.to be_present
.and have_attributes(status_ids: [status.id])
expect(report.comment)
.to have_attributes(length: described_class::COMMENT_SIZE_LIMIT)
.and eq(long_comment[0...described_class::COMMENT_SIZE_LIMIT])
end
end

View file

@ -3,6 +3,9 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Move do
RSpec::Matchers.define_negated_matcher :not_be_following, :be_following
RSpec::Matchers.define_negated_matcher :not_be_requested, :be_requested
let(:follower) { Fabricate(:account) }
let(:old_account) { Fabricate(:account, uri: 'https://example.org/alice', domain: 'example.org', protocol: :activitypub, inbox_url: 'https://example.org/inbox') }
let(:new_account) { Fabricate(:account, uri: 'https://example.com/alice', domain: 'example.com', protocol: :activitypub, inbox_url: 'https://example.com/inbox', also_known_as: also_known_as) }
@ -38,49 +41,37 @@ RSpec.describe ActivityPub::Activity::Move do
subject.perform
end
context 'when all conditions are met' do
it 'sets moved account on old account' do
expect(old_account.reload.moved_to_account_id).to eq new_account.id
end
it 'makes followers unfollow old account' do
expect(follower.following?(old_account)).to be false
end
it 'makes followers follow-request the new account' do
expect(follower.requested?(new_account)).to be true
context 'when all conditions are met', :inline_jobs do
it 'sets moved on old account, followers unfollow old account, followers request the new account' do
expect(old_account.reload.moved_to_account_id)
.to eq new_account.id
expect(follower)
.to not_be_following(old_account)
.and be_requested(new_account)
end
end
context "when the new account can't be resolved" do
let(:returned_account) { nil }
it 'does not set moved account on old account' do
expect(old_account.reload.moved_to_account_id).to be_nil
end
it 'does not make followers unfollow old account' do
expect(follower.following?(old_account)).to be true
end
it 'does not make followers follow-request the new account' do
expect(follower.requested?(new_account)).to be false
it 'does not set moved on old account, does not unfollow old, does not follow request new' do
expect(old_account.reload.moved_to_account_id)
.to be_nil
expect(follower)
.to be_following(old_account)
.and not_be_requested(new_account)
end
end
context 'when the new account does not references the old account' do
let(:also_known_as) { [] }
it 'does not set moved account on old account' do
expect(old_account.reload.moved_to_account_id).to be_nil
end
it 'does not make followers unfollow old account' do
expect(follower.following?(old_account)).to be true
end
it 'does not make followers follow-request the new account' do
expect(follower.requested?(new_account)).to be false
it 'does not set moved on old account, does not unfollow old, does not follow request new' do
expect(old_account.reload.moved_to_account_id)
.to be_nil
expect(follower)
.to be_following(old_account)
.and not_be_requested(new_account)
end
end
@ -91,16 +82,12 @@ RSpec.describe ActivityPub::Activity::Move do
redis.del("move_in_progress:#{old_account.id}")
end
it 'does not set moved account on old account' do
expect(old_account.reload.moved_to_account_id).to be_nil
end
it 'does not make followers unfollow old account' do
expect(follower.following?(old_account)).to be true
end
it 'does not make followers follow-request the new account' do
expect(follower.requested?(new_account)).to be false
it 'does not set moved on old account, does not unfollow old, does not follow request new' do
expect(old_account.reload.moved_to_account_id)
.to be_nil
expect(follower)
.to be_following(old_account)
.and not_be_requested(new_account)
end
end
end

View file

@ -53,7 +53,7 @@ RSpec.describe ActivityPub::Adapter do
describe '#serializable_hash' do
subject { ActiveModelSerializers::SerializableResource.new(TestObject.new(foo: 'bar'), serializer: serializer_class, adapter: described_class).as_json }
let(:serializer_class) {}
let(:serializer_class) { nil }
context 'when serializer defines no context' do
let(:serializer_class) { TestWithBasicContextSerializer }

View file

@ -18,10 +18,6 @@ RSpec.describe ActivityPub::LinkedDataSignature do
let(:json) { raw_json.merge('signature' => signature) }
before do
stub_jsonld_contexts!
end
describe '#verify_actor!' do
context 'when signature matches' do
let(:raw_signature) do
@ -99,16 +95,11 @@ RSpec.describe ActivityPub::LinkedDataSignature do
describe '#sign!' do
subject { described_class.new(raw_json).sign!(sender) }
it 'returns a hash' do
it 'returns a hash with a signature, the expected context, and the signature can be verified', :aggregate_failures do
expect(subject).to be_a Hash
end
it 'contains signature' do
expect(subject['signature']).to be_a Hash
expect(subject['signature']['signatureValue']).to be_present
end
it 'can be verified again' do
expect(Array(subject['@context'])).to include('https://w3id.org/security/v1')
expect(described_class.new(subject).verify_actor!).to eq sender
end
end

View file

@ -52,20 +52,27 @@ RSpec.describe ActivityPub::TagManager do
expect(subject.to(status)).to include(subject.followers_uri_for(mentioned))
end
it "returns URIs of mentions for direct silenced author's status only if they are followers or requesting to be" do
bob = Fabricate(:account, username: 'bob')
alice = Fabricate(:account, username: 'alice')
foo = Fabricate(:account)
author = Fabricate(:account, username: 'author', silenced: true)
status = Fabricate(:status, visibility: :direct, account: author)
bob.follow!(author)
FollowRequest.create!(account: foo, target_account: author)
status.mentions.create(account: alice)
status.mentions.create(account: bob)
status.mentions.create(account: foo)
expect(subject.to(status)).to include(subject.uri_for(bob))
expect(subject.to(status)).to include(subject.uri_for(foo))
expect(subject.to(status)).to_not include(subject.uri_for(alice))
context 'with followers and requested followers' do
let!(:bob) { Fabricate(:account, username: 'bob') }
let!(:alice) { Fabricate(:account, username: 'alice') }
let!(:foo) { Fabricate(:account) }
let!(:author) { Fabricate(:account, username: 'author', silenced: true) }
let!(:status) { Fabricate(:status, visibility: :direct, account: author) }
before do
bob.follow!(author)
FollowRequest.create!(account: foo, target_account: author)
status.mentions.create(account: alice)
status.mentions.create(account: bob)
status.mentions.create(account: foo)
end
it "returns URIs of mentions for direct silenced author's status only if they are followers or requesting to be" do
expect(subject.to(status))
.to include(subject.uri_for(bob))
.and include(subject.uri_for(foo))
.and not_include(subject.uri_for(alice))
end
end
end
@ -97,20 +104,35 @@ RSpec.describe ActivityPub::TagManager do
expect(subject.cc(status)).to include(subject.uri_for(mentioned))
end
it "returns URIs of mentions for silenced author's non-direct status only if they are followers or requesting to be" do
bob = Fabricate(:account, username: 'bob')
context 'with followers and requested followers' do
let!(:bob) { Fabricate(:account, username: 'bob') }
let!(:alice) { Fabricate(:account, username: 'alice') }
let!(:foo) { Fabricate(:account) }
let!(:author) { Fabricate(:account, username: 'author', silenced: true) }
let!(:status) { Fabricate(:status, visibility: :public, account: author) }
before do
bob.follow!(author)
FollowRequest.create!(account: foo, target_account: author)
status.mentions.create(account: alice)
status.mentions.create(account: bob)
status.mentions.create(account: foo)
end
it "returns URIs of mentions for silenced author's non-direct status only if they are followers or requesting to be" do
expect(subject.cc(status))
.to include(subject.uri_for(bob))
.and include(subject.uri_for(foo))
.and not_include(subject.uri_for(alice))
end
end
it 'returns poster of reblogged post, if reblog' do
bob = Fabricate(:account, username: 'bob', domain: 'example.com', inbox_url: 'http://example.com/bob')
alice = Fabricate(:account, username: 'alice')
foo = Fabricate(:account)
author = Fabricate(:account, username: 'author', silenced: true)
status = Fabricate(:status, visibility: :public, account: author)
bob.follow!(author)
FollowRequest.create!(account: foo, target_account: author)
status.mentions.create(account: alice)
status.mentions.create(account: bob)
status.mentions.create(account: foo)
expect(subject.cc(status)).to include(subject.uri_for(bob))
expect(subject.cc(status)).to include(subject.uri_for(foo))
expect(subject.cc(status)).to_not include(subject.uri_for(alice))
status = Fabricate(:status, visibility: :public, account: bob)
reblog = Fabricate(:status, visibility: :public, account: alice, reblog: status)
expect(subject.cc(reblog)).to include(subject.uri_for(bob))
end
it 'returns poster of reblogged post, if reblog' do

View file

@ -2,17 +2,33 @@
require 'rails_helper'
describe Admin::Metrics::Dimension::InstanceAccountsDimension do
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
RSpec.describe Admin::Metrics::Dimension::InstanceAccountsDimension do
subject { described_class.new(start_at, end_at, limit, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
let(:limit) { 10 }
let(:params) { ActionController::Parameters.new }
let(:params) { ActionController::Parameters.new(domain: domain) }
describe '#data' do
it 'runs data query without error' do
expect { dimension.data }.to_not raise_error
let(:domain) { 'host.example' }
let(:alice) { Fabricate(:account, domain: domain) }
let(:bob) { Fabricate(:account) }
before do
Fabricate :follow, target_account: alice
Fabricate :follow, target_account: bob
Fabricate :status, account: alice
Fabricate :status, account: bob
end
it 'returns instances with follow counts' do
expect(subject.data.size)
.to eq(1)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(key: alice.username, value: '1')
)
end
end
end

View file

@ -2,17 +2,31 @@
require 'rails_helper'
describe Admin::Metrics::Dimension::InstanceLanguagesDimension do
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
RSpec.describe Admin::Metrics::Dimension::InstanceLanguagesDimension do
subject { described_class.new(start_at, end_at, limit, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
let(:limit) { 10 }
let(:params) { ActionController::Parameters.new }
let(:params) { ActionController::Parameters.new(domain: domain) }
describe '#data' do
it 'runs data query without error' do
expect { dimension.data }.to_not raise_error
let(:domain) { 'host.example' }
let(:alice) { Fabricate(:account, domain: domain) }
let(:bob) { Fabricate(:account) }
before do
Fabricate :status, account: alice, language: 'en'
Fabricate :status, account: bob, language: 'es'
end
it 'returns locales with status counts' do
expect(subject.data.size)
.to eq(1)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(key: 'en', value: '1')
)
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Dimension::LanguagesDimension do
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
RSpec.describe Admin::Metrics::Dimension::LanguagesDimension do
subject { described_class.new(start_at, end_at, limit, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
@ -11,8 +11,21 @@ describe Admin::Metrics::Dimension::LanguagesDimension do
let(:params) { ActionController::Parameters.new }
describe '#data' do
it 'runs data query without error' do
expect { dimension.data }.to_not raise_error
let(:alice) { Fabricate(:user, locale: 'en', current_sign_in_at: 1.day.ago) }
let(:bob) { Fabricate(:user, locale: 'en', current_sign_in_at: 30.days.ago) }
before do
alice.update(current_sign_in_at: 1.day.ago)
bob.update(current_sign_in_at: 30.days.ago)
end
it 'returns locales with sign in counts' do
expect(subject.data.size)
.to eq(1)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(key: 'en', value: '1')
)
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Dimension::ServersDimension do
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
RSpec.describe Admin::Metrics::Dimension::ServersDimension do
subject { described_class.new(start_at, end_at, limit, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
@ -11,8 +11,24 @@ describe Admin::Metrics::Dimension::ServersDimension do
let(:params) { ActionController::Parameters.new }
describe '#data' do
it 'runs data query without error' do
expect { dimension.data }.to_not raise_error
let(:domain) { 'host.example' }
let(:alice) { Fabricate(:account, domain: domain) }
let(:bob) { Fabricate(:account) }
before do
Fabricate :status, account: alice, created_at: 1.day.ago
Fabricate :status, account: alice, created_at: 30.days.ago
Fabricate :status, account: bob, created_at: 1.day.ago
end
it 'returns domains with status counts' do
expect(subject.data.size)
.to eq(2)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(key: domain, value: '1'),
include(key: Rails.configuration.x.local_domain, value: '1')
)
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Dimension::SoftwareVersionsDimension do
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
RSpec.describe Admin::Metrics::Dimension::SoftwareVersionsDimension do
subject { described_class.new(start_at, end_at, limit, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
@ -11,8 +11,12 @@ describe Admin::Metrics::Dimension::SoftwareVersionsDimension do
let(:params) { ActionController::Parameters.new }
describe '#data' do
it 'runs data query without error' do
expect { dimension.data }.to_not raise_error
it 'reports on the running software' do
expect(subject.data.map(&:symbolize_keys))
.to include(
include(key: 'mastodon', value: Mastodon::Version.to_s),
include(key: 'ruby', value: include(RUBY_VERSION))
)
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Dimension::SourcesDimension do
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
RSpec.describe Admin::Metrics::Dimension::SourcesDimension do
subject { described_class.new(start_at, end_at, limit, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
@ -11,8 +11,21 @@ describe Admin::Metrics::Dimension::SourcesDimension do
let(:params) { ActionController::Parameters.new }
describe '#data' do
it 'runs data query without error' do
expect { dimension.data }.to_not raise_error
let(:app) { Fabricate(:application) }
let(:alice) { Fabricate(:user) }
let(:bob) { Fabricate(:user) }
before do
alice.update(created_by_application: app)
end
it 'returns OAuth applications with user counts' do
expect(subject.data.size)
.to eq(1)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(key: app.name, value: '1')
)
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Dimension::SpaceUsageDimension do
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
RSpec.describe Admin::Metrics::Dimension::SpaceUsageDimension do
subject { described_class.new(start_at, end_at, limit, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
@ -11,8 +11,13 @@ describe Admin::Metrics::Dimension::SpaceUsageDimension do
let(:params) { ActionController::Parameters.new }
describe '#data' do
it 'runs data query without error' do
expect { dimension.data }.to_not raise_error
it 'reports on used storage space' do
expect(subject.data.map(&:symbolize_keys))
.to include(
include(key: 'media', value: /\d/),
include(key: 'postgresql', value: /\d/),
include(key: 'redis', value: /\d/)
)
end
end
end

View file

@ -2,17 +2,37 @@
require 'rails_helper'
describe Admin::Metrics::Dimension::TagLanguagesDimension do
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
RSpec.describe Admin::Metrics::Dimension::TagLanguagesDimension do
subject { described_class.new(start_at, end_at, limit, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
let(:limit) { 10 }
let(:params) { ActionController::Parameters.new }
let(:params) { ActionController::Parameters.new(id: tag.id) }
describe '#data' do
it 'runs data query without error' do
expect { dimension.data }.to_not raise_error
let(:alice) { Fabricate(:account) }
let(:bob) { Fabricate(:account) }
let(:tag) { Fabricate(:tag) }
before do
alice_status_recent = Fabricate :status, account: alice, created_at: 1.day.ago, language: 'en'
alice_status_older = Fabricate :status, account: alice, created_at: 30.days.ago, language: 'en'
bob_status_recent = Fabricate :status, account: bob, created_at: 1.day.ago, language: 'es'
alice_status_older.tags << tag
alice_status_recent.tags << tag
bob_status_recent.tags << tag
end
it 'returns languages with tag usage counts' do
expect(subject.data.size)
.to eq(2)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(key: 'en', value: '1'),
include(key: 'es', value: '1')
)
end
end
end

View file

@ -2,17 +2,38 @@
require 'rails_helper'
describe Admin::Metrics::Dimension::TagServersDimension do
subject(:dimension) { described_class.new(start_at, end_at, limit, params) }
RSpec.describe Admin::Metrics::Dimension::TagServersDimension do
subject { described_class.new(start_at, end_at, limit, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
let(:limit) { 10 }
let(:params) { ActionController::Parameters.new }
let(:params) { ActionController::Parameters.new(id: tag.id) }
describe '#data' do
it 'runs data query without error' do
expect { dimension.data }.to_not raise_error
let(:alice) { Fabricate(:account, domain: domain) }
let(:bob) { Fabricate(:account) }
let(:domain) { 'host.example' }
let(:tag) { Fabricate(:tag) }
before do
alice_status_recent = Fabricate :status, account: alice, created_at: 1.day.ago
alice_status_older = Fabricate :status, account: alice, created_at: 30.days.ago
bob_status_recent = Fabricate :status, account: bob, created_at: 1.day.ago
alice_status_older.tags << tag
alice_status_recent.tags << tag
bob_status_recent.tags << tag
end
it 'returns servers with tag usage counts' do
expect(subject.data.size)
.to eq(2)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(key: domain, value: '1'),
include(key: Rails.configuration.x.local_domain, value: '1')
)
end
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::Metrics::Dimension do
describe '.retrieve' do
subject { described_class.retrieve(reports, start_at, end_at, 5, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
let(:params) { ActionController::Parameters.new({ instance_accounts: [123], instance_languages: ['en'] }) }
let(:reports) { [:instance_accounts, :instance_languages] }
it 'returns instances of provided classes' do
expect(subject)
.to contain_exactly(
be_a(Admin::Metrics::Dimension::InstanceAccountsDimension),
be_a(Admin::Metrics::Dimension::InstanceLanguagesDimension)
)
end
end
end

View file

@ -2,16 +2,39 @@
require 'rails_helper'
describe Admin::Metrics::Measure::ActiveUsersMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::ActiveUsersMeasure do
subject { described_class.new(start_at, end_at, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
let(:params) { ActionController::Parameters.new }
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
context 'with activity tracking records' do
before do
3.times do
travel_to(2.days.ago) { record_login_activity }
end
2.times do
travel_to(1.day.ago) { record_login_activity }
end
travel_to(0.days.ago) { record_login_activity }
end
it 'returns correct activity tracker counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '3'),
include(date: 1.day.ago.midnight.to_time, value: '2'),
include(date: 0.days.ago.midnight.to_time, value: '1')
)
end
def record_login_activity
ActivityTracker.record('activity:logins', Fabricate(:user).id)
end
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Measure::InstanceAccountsMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::InstanceAccountsMeasure do
subject { described_class.new(start_at, end_at, params) }
let(:domain) { 'example.com' }
@ -20,12 +20,13 @@ describe Admin::Metrics::Measure::InstanceAccountsMeasure do
Fabricate(:account, domain: "foo.#{domain}", created_at: 1.year.ago)
Fabricate(:account, domain: "foo.#{domain}")
Fabricate(:account, domain: "bar.#{domain}")
Fabricate(:account, domain: 'other-host.example')
end
describe 'total' do
describe '#total' do
context 'without include_subdomains' do
it 'returns the expected number of accounts' do
expect(measure.total).to eq 3
expect(subject.total).to eq 3
end
end
@ -33,14 +34,21 @@ describe Admin::Metrics::Measure::InstanceAccountsMeasure do
let(:params) { ActionController::Parameters.new(domain: domain, include_subdomains: 'true') }
it 'returns the expected number of accounts' do
expect(measure.total).to eq 6
expect(subject.total).to eq 6
end
end
end
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
it 'returns correct instance_accounts counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '0'),
include(date: 1.day.ago.midnight.to_time, value: '0'),
include(date: 0.days.ago.midnight.to_time, value: '1')
)
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Measure::InstanceFollowersMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::InstanceFollowersMeasure do
subject { described_class.new(start_at, end_at, params) }
let(:domain) { 'example.com' }
@ -22,12 +22,14 @@ describe Admin::Metrics::Measure::InstanceFollowersMeasure do
Fabricate(:account, domain: "foo.#{domain}").follow!(local_account)
Fabricate(:account, domain: "foo.#{domain}").follow!(local_account)
Fabricate(:account, domain: "bar.#{domain}")
Fabricate(:account, domain: 'other.example').follow!(local_account)
end
describe 'total' do
describe '#total' do
context 'without include_subdomains' do
it 'returns the expected number of accounts' do
expect(measure.total).to eq 2
expect(subject.total).to eq 2
end
end
@ -35,14 +37,21 @@ describe Admin::Metrics::Measure::InstanceFollowersMeasure do
let(:params) { ActionController::Parameters.new(domain: domain, include_subdomains: 'true') }
it 'returns the expected number of accounts' do
expect(measure.total).to eq 4
expect(subject.total).to eq 4
end
end
end
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
it 'returns correct instance_followers counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '0'),
include(date: 1.day.ago.midnight.to_time, value: '0'),
include(date: 0.days.ago.midnight.to_time, value: '2')
)
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Measure::InstanceFollowsMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::InstanceFollowsMeasure do
subject { described_class.new(start_at, end_at, params) }
let(:domain) { 'example.com' }
@ -24,10 +24,10 @@ describe Admin::Metrics::Measure::InstanceFollowsMeasure do
Fabricate(:account, domain: "bar.#{domain}")
end
describe 'total' do
describe '#total' do
context 'without include_subdomains' do
it 'returns the expected number of accounts' do
expect(measure.total).to eq 2
expect(subject.total).to eq 2
end
end
@ -35,14 +35,21 @@ describe Admin::Metrics::Measure::InstanceFollowsMeasure do
let(:params) { ActionController::Parameters.new(domain: domain, include_subdomains: 'true') }
it 'returns the expected number of accounts' do
expect(measure.total).to eq 4
expect(subject.total).to eq 4
end
end
end
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
it 'returns correct instance_followers counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '0'),
include(date: 1.day.ago.midnight.to_time, value: '0'),
include(date: 0.days.ago.midnight.to_time, value: '2')
)
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure do
subject { described_class.new(start_at, end_at, params) }
let(:domain) { 'example.com' }
@ -20,11 +20,11 @@ describe Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure do
remote_account_on_subdomain.media_attachments.create!(file: attachment_fixture('attachment.jpg'))
end
describe 'total' do
describe '#total' do
context 'without include_subdomains' do
it 'returns the expected number of accounts' do
expected_total = remote_account.media_attachments.sum(:file_file_size) + remote_account.media_attachments.sum(:thumbnail_file_size)
expect(measure.total).to eq expected_total
expect(subject.total).to eq expected_total
end
end
@ -36,14 +36,25 @@ describe Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure do
account.media_attachments.sum(:file_file_size) + account.media_attachments.sum(:thumbnail_file_size)
end
expect(measure.total).to eq expected_total
expect(subject.total).to eq expected_total
end
end
end
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
it 'returns correct media_attachments counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '0'),
include(date: 1.day.ago.midnight.to_time, value: '0'),
include(date: 0.days.ago.midnight.to_time, value: expected_domain_only_total.to_s)
)
end
def expected_domain_only_total
remote_account.media_attachments.sum(:file_file_size) + remote_account.media_attachments.sum(:thumbnail_file_size)
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Measure::InstanceReportsMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::InstanceReportsMeasure do
subject { described_class.new(start_at, end_at, params) }
let(:domain) { 'example.com' }
@ -21,10 +21,10 @@ describe Admin::Metrics::Measure::InstanceReportsMeasure do
Fabricate(:report, target_account: Fabricate(:account, domain: "bar.#{domain}"))
end
describe 'total' do
describe '#total' do
context 'without include_subdomains' do
it 'returns the expected number of accounts' do
expect(measure.total).to eq 2
expect(subject.total).to eq 2
end
end
@ -32,14 +32,21 @@ describe Admin::Metrics::Measure::InstanceReportsMeasure do
let(:params) { ActionController::Parameters.new(domain: domain, include_subdomains: 'true') }
it 'returns the expected number of accounts' do
expect(measure.total).to eq 5
expect(subject.total).to eq 5
end
end
end
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
it 'returns correct instance_reports counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '0'),
include(date: 1.day.ago.midnight.to_time, value: '0'),
include(date: 0.days.ago.midnight.to_time, value: '2')
)
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Measure::InstanceStatusesMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::InstanceStatusesMeasure do
subject { described_class.new(start_at, end_at, params) }
let(:domain) { 'example.com' }
@ -21,10 +21,10 @@ describe Admin::Metrics::Measure::InstanceStatusesMeasure do
Fabricate(:status, account: Fabricate(:account, domain: "bar.#{domain}"))
end
describe 'total' do
describe '#total' do
context 'without include_subdomains' do
it 'returns the expected number of accounts' do
expect(measure.total).to eq 2
expect(subject.total).to eq 2
end
end
@ -32,14 +32,21 @@ describe Admin::Metrics::Measure::InstanceStatusesMeasure do
let(:params) { ActionController::Parameters.new(domain: domain, include_subdomains: 'true') }
it 'returns the expected number of accounts' do
expect(measure.total).to eq 5
expect(subject.total).to eq 5
end
end
end
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
it 'returns correct instance_statuses counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '0'),
include(date: 1.day.ago.midnight.to_time, value: '0'),
include(date: 0.days.ago.midnight.to_time, value: '2')
)
end
end
end

View file

@ -2,16 +2,39 @@
require 'rails_helper'
describe Admin::Metrics::Measure::InteractionsMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::InteractionsMeasure do
subject { described_class.new(start_at, end_at, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
let(:params) { ActionController::Parameters.new }
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
context 'with activity tracking records' do
before do
3.times do
travel_to(2.days.ago) { record_interaction_activity }
end
2.times do
travel_to(1.day.ago) { record_interaction_activity }
end
travel_to(0.days.ago) { record_interaction_activity }
end
it 'returns correct activity tracker counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '3'),
include(date: 1.day.ago.midnight.to_time, value: '2'),
include(date: 0.days.ago.midnight.to_time, value: '1')
)
end
def record_interaction_activity
ActivityTracker.increment('activity:interactions')
end
end
end
end

View file

@ -2,16 +2,31 @@
require 'rails_helper'
describe Admin::Metrics::Measure::NewUsersMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::NewUsersMeasure do
subject { described_class.new(start_at, end_at, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
let(:params) { ActionController::Parameters.new }
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
context 'with user records' do
before do
3.times { Fabricate :user, created_at: 2.days.ago }
2.times { Fabricate :user, created_at: 1.day.ago }
Fabricate :user, created_at: 0.days.ago
end
it 'returns correct user counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '3'),
include(date: 1.day.ago.midnight.to_time, value: '2'),
include(date: 0.days.ago.midnight.to_time, value: '1')
)
end
end
end
end

View file

@ -2,16 +2,31 @@
require 'rails_helper'
describe Admin::Metrics::Measure::OpenedReportsMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::OpenedReportsMeasure do
subject { described_class.new(start_at, end_at, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
let(:params) { ActionController::Parameters.new }
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
context 'with report records' do
before do
3.times { Fabricate :report, created_at: 2.days.ago }
2.times { Fabricate :report, created_at: 1.day.ago }
Fabricate :report, created_at: 0.days.ago
end
it 'returns correct report counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '3'),
include(date: 1.day.ago.midnight.to_time, value: '2'),
include(date: 0.days.ago.midnight.to_time, value: '1')
)
end
end
end
end

View file

@ -2,16 +2,31 @@
require 'rails_helper'
describe Admin::Metrics::Measure::ResolvedReportsMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::ResolvedReportsMeasure do
subject { described_class.new(start_at, end_at, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
let(:params) { ActionController::Parameters.new }
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
context 'with report records' do
before do
3.times { Fabricate :report, action_taken_at: 2.days.ago }
2.times { Fabricate :report, action_taken_at: 1.day.ago }
Fabricate :report, action_taken_at: 0.days.ago
end
it 'returns correct report counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '3'),
include(date: 1.day.ago.midnight.to_time, value: '2'),
include(date: 0.days.ago.midnight.to_time, value: '1')
)
end
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Measure::TagAccountsMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::TagAccountsMeasure do
subject { described_class.new(start_at, end_at, params) }
let!(:tag) { Fabricate(:tag) }
@ -12,8 +12,39 @@ describe Admin::Metrics::Measure::TagAccountsMeasure do
let(:params) { ActionController::Parameters.new(id: tag.id) }
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
context 'with tagged accounts' do
let(:alice) { Fabricate(:account, domain: 'alice.example') }
let(:bob) { Fabricate(:account, domain: 'bob.example') }
before do
3.times do
travel_to(2.days.ago) { add_tag_history(alice) }
end
2.times do
travel_to(1.day.ago) do
add_tag_history(alice)
add_tag_history(bob)
end
end
add_tag_history(bob)
end
it 'returns correct tag_accounts counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '1'),
include(date: 1.day.ago.midnight.to_time, value: '2'),
include(date: 0.days.ago.midnight.to_time, value: '1')
)
end
def add_tag_history(account)
tag.history.add(account.id)
end
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Measure::TagServersMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::TagServersMeasure do
subject { described_class.new(start_at, end_at, params) }
let!(:tag) { Fabricate(:tag) }
@ -12,8 +12,38 @@ describe Admin::Metrics::Measure::TagServersMeasure do
let(:params) { ActionController::Parameters.new(id: tag.id) }
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
context 'with tagged statuses' do
let(:alice) { Fabricate(:account, domain: 'alice.example') }
let(:bob) { Fabricate(:account, domain: 'bob.example') }
before do
3.times do
status_alice = Fabricate(:status, account: alice, created_at: 2.days.ago)
status_alice.tags << tag
end
2.times do
status_alice = Fabricate(:status, account: alice, created_at: 1.day.ago)
status_alice.tags << tag
status_bob = Fabricate(:status, account: bob, created_at: 1.day.ago)
status_bob.tags << tag
end
status_bob = Fabricate(:status, account: bob, created_at: 0.days.ago)
status_bob.tags << tag
end
it 'returns correct tag counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '1'),
include(date: 1.day.ago.midnight.to_time, value: '2'),
include(date: 0.days.ago.midnight.to_time, value: '1')
)
end
end
end
end

View file

@ -2,8 +2,8 @@
require 'rails_helper'
describe Admin::Metrics::Measure::TagUsesMeasure do
subject(:measure) { described_class.new(start_at, end_at, params) }
RSpec.describe Admin::Metrics::Measure::TagUsesMeasure do
subject { described_class.new(start_at, end_at, params) }
let!(:tag) { Fabricate(:tag) }
@ -12,8 +12,39 @@ describe Admin::Metrics::Measure::TagUsesMeasure do
let(:params) { ActionController::Parameters.new(id: tag.id) }
describe '#data' do
it 'runs data query without error' do
expect { measure.data }.to_not raise_error
context 'with tagged accounts' do
let(:alice) { Fabricate(:account, domain: 'alice.example') }
let(:bob) { Fabricate(:account, domain: 'bob.example') }
before do
3.times do
travel_to(2.days.ago) { add_tag_history(alice) }
end
2.times do
travel_to(1.day.ago) do
add_tag_history(alice)
add_tag_history(bob)
end
end
add_tag_history(bob)
end
it 'returns correct tag_uses counts' do
expect(subject.data.size)
.to eq(3)
expect(subject.data.map(&:symbolize_keys))
.to contain_exactly(
include(date: 2.days.ago.midnight.to_time, value: '3'),
include(date: 1.day.ago.midnight.to_time, value: '4'),
include(date: 0.days.ago.midnight.to_time, value: '1')
)
end
def add_tag_history(account)
tag.history.add(account.id)
end
end
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::Metrics::Measure do
describe '.retrieve' do
subject { described_class.retrieve(reports, start_at, end_at, params) }
let(:start_at) { 2.days.ago }
let(:end_at) { Time.now.utc }
let(:params) { ActionController::Parameters.new({ instance_accounts: [123], instance_followers: [123] }) }
let(:reports) { [:instance_accounts, :instance_followers] }
it 'returns instances of provided classes' do
expect(subject)
.to contain_exactly(
be_a(Admin::Metrics::Measure::InstanceAccountsMeasure),
be_a(Admin::Metrics::Measure::InstanceFollowersMeasure)
)
end
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Admin::SystemCheck::BaseCheck do
RSpec.describe Admin::SystemCheck::BaseCheck do
subject(:check) { described_class.new(user) }
let(:user) { Fabricate(:user) }

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Admin::SystemCheck::DatabaseSchemaCheck do
RSpec.describe Admin::SystemCheck::DatabaseSchemaCheck do
subject(:check) { described_class.new(user) }
let(:user) { Fabricate(:user) }

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Admin::SystemCheck::ElasticsearchCheck do
RSpec.describe Admin::SystemCheck::ElasticsearchCheck do
subject(:check) { described_class.new(user) }
let(:user) { Fabricate(:user) }
@ -127,7 +127,7 @@ describe Admin::SystemCheck::ElasticsearchCheck do
end
def stub_elasticsearch_error
client = instance_double(Elasticsearch::Transport::Client)
client = instance_double(Elasticsearch::Client)
allow(client).to receive(:info).and_raise(Elasticsearch::Transport::Transport::Error)
allow(Chewy).to receive(:client).and_return(client)
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Admin::SystemCheck::MediaPrivacyCheck do
RSpec.describe Admin::SystemCheck::MediaPrivacyCheck do
subject(:check) { described_class.new(user) }
let(:user) { Fabricate(:user) }

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Admin::SystemCheck::Message do
RSpec.describe Admin::SystemCheck::Message do
subject(:check) { described_class.new(:key_value, :value_value, :action_value, :critical_value) }
it 'providers readers when initialized' do

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Admin::SystemCheck::RulesCheck do
RSpec.describe Admin::SystemCheck::RulesCheck do
subject(:check) { described_class.new(user) }
let(:user) { Fabricate(:user) }

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Admin::SystemCheck::SidekiqProcessCheck do
RSpec.describe Admin::SystemCheck::SidekiqProcessCheck do
subject(:check) { described_class.new(user) }
let(:user) { Fabricate(:user) }

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Admin::SystemCheck::SoftwareVersionCheck do
RSpec.describe Admin::SystemCheck::SoftwareVersionCheck do
include RoutingHelper
subject(:check) { described_class.new(user) }
@ -51,8 +51,8 @@ describe Admin::SystemCheck::SoftwareVersionCheck do
Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: false)
end
it 'returns true' do
expect(check.pass?).to be true
it 'returns false' do
expect(check.pass?).to be false
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Admin::SystemCheck do
RSpec.describe Admin::SystemCheck do
let(:user) { Fabricate(:user) }
describe 'perform' do

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AnnualReport::CommonlyInteractedWithAccounts do
describe '#generate' do
subject { described_class.new(account, Time.zone.now.year) }
context 'with an inactive account' do
let(:account) { Fabricate :account }
it 'builds a report for an account' do
expect(subject.generate)
.to include(
commonly_interacted_with_accounts: be_an(Array).and(be_empty)
)
end
end
context 'with an active account' do
let(:account) { Fabricate :account }
let(:other_account) { Fabricate :account }
before do
_other = Fabricate :status
Fabricate :status, account: account, reply: true, in_reply_to_id: Fabricate(:status, account: other_account).id
Fabricate :status, account: account, reply: true, in_reply_to_id: Fabricate(:status, account: other_account).id
end
it 'builds a report for an account' do
expect(subject.generate)
.to include(
commonly_interacted_with_accounts: contain_exactly(
include(account_id: other_account.id, count: 2)
)
)
end
end
end
end

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AnnualReport::MostRebloggedAccounts do
describe '#generate' do
subject { described_class.new(account, Time.zone.now.year) }
context 'with an inactive account' do
let(:account) { Fabricate :account }
it 'builds a report for an account' do
expect(subject.generate)
.to include(
most_reblogged_accounts: be_an(Array).and(be_empty)
)
end
end
context 'with an active account' do
let(:account) { Fabricate :account }
let(:other_account) { Fabricate :account }
before do
_other = Fabricate :status
Fabricate :status, account: account, reblog: Fabricate(:status, account: other_account)
Fabricate :status, account: account, reblog: Fabricate(:status, account: other_account)
end
it 'builds a report for an account' do
expect(subject.generate)
.to include(
most_reblogged_accounts: contain_exactly(
include(account_id: other_account.id, count: 2)
)
)
end
end
end
end

View file

@ -0,0 +1,40 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AnnualReport::MostUsedApps do
describe '#generate' do
subject { described_class.new(account, Time.zone.now.year) }
context 'with an inactive account' do
let(:account) { Fabricate :account }
it 'builds a report for an account' do
expect(subject.generate)
.to include(
most_used_apps: be_an(Array).and(be_empty)
)
end
end
context 'with an active account' do
let(:account) { Fabricate :account }
let(:application) { Fabricate :application }
before do
_other = Fabricate :status
Fabricate.times 2, :status, account: account, application: application
end
it 'builds a report for an account' do
expect(subject.generate)
.to include(
most_used_apps: contain_exactly(
include(name: application.name, count: 2)
)
)
end
end
end
end

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AnnualReport::Percentiles do
describe '#generate' do
subject { described_class.new(account, Time.zone.now.year) }
context 'with an inactive account' do
let(:account) { Fabricate :account }
it 'builds a report for an account' do
expect(subject.generate)
.to include(
percentiles: include(
followers: 0,
statuses: 0
)
)
end
end
context 'with an active account' do
let(:account) { Fabricate :account }
before do
Fabricate.times 2, :status # Others as `account`
Fabricate.times 2, :follow # Others as `target_account`
Fabricate.times 2, :status, account: account
Fabricate.times 2, :follow, target_account: account
end
it 'builds a report for an account' do
expect(subject.generate)
.to include(
percentiles: include(
followers: 50,
statuses: 50
)
)
end
end
end
end

View file

@ -0,0 +1,46 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AnnualReport::TimeSeries do
describe '#generate' do
subject { described_class.new(account, Time.zone.now.year) }
context 'with an inactive account' do
let(:account) { Fabricate :account }
it 'builds a report for an account' do
expect(subject.generate)
.to include(
time_series: match(
include(followers: 0, following: 0, month: 1, statuses: 0)
)
)
end
end
context 'with an active account' do
let(:account) { Fabricate :account }
let(:month_one_date) { DateTime.new(Time.zone.now.year, 1, 1, 12, 12, 12) }
let(:tag) { Fabricate :tag }
before do
_other = Fabricate :status
Fabricate :status, account: account, created_at: month_one_date
Fabricate :follow, account: account, created_at: month_one_date
Fabricate :follow, target_account: account, created_at: month_one_date
end
it 'builds a report for an account' do
expect(subject.generate)
.to include(
time_series: match(
include(followers: 1, following: 1, month: 1, statuses: 1)
)
)
end
end
end
end

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AnnualReport::TopHashtags do
describe '#generate' do
subject { described_class.new(account, Time.zone.now.year) }
context 'with an inactive account' do
let(:account) { Fabricate :account }
it 'builds a report for an account' do
expect(subject.generate)
.to include(
top_hashtags: be_an(Array).and(be_empty)
)
end
end
context 'with an active account' do
let(:account) { Fabricate :account }
let(:tag) { Fabricate :tag }
before do
_other = Fabricate :status
first = Fabricate :status, account: account
first.tags << tag
last = Fabricate :status, account: account
last.tags << tag
end
it 'builds a report for an account' do
expect(subject.generate)
.to include(
top_hashtags: contain_exactly(
include(name: tag.name, count: 2)
)
)
end
end
end
end

View file

@ -0,0 +1,50 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AnnualReport::TopStatuses do
describe '#generate' do
subject { described_class.new(account, Time.zone.now.year) }
context 'with an inactive account' do
let(:account) { Fabricate :account }
it 'builds a report for an account' do
expect(subject.generate)
.to include(
top_statuses: include(
by_reblogs: be_nil,
by_favourites: be_nil,
by_replies: be_nil
)
)
end
end
context 'with an active account' do
let(:account) { Fabricate :account }
let(:reblogged_status) { Fabricate :status, account: account }
let(:favourited_status) { Fabricate :status, account: account }
let(:replied_status) { Fabricate :status, account: account }
before do
_other = Fabricate :status
reblogged_status.status_stat.update(reblogs_count: 123)
favourited_status.status_stat.update(favourites_count: 123)
replied_status.status_stat.update(replies_count: 123)
end
it 'builds a report for an account' do
expect(subject.generate)
.to include(
top_statuses: include(
by_reblogs: reblogged_status.id,
by_favourites: favourited_status.id,
by_replies: replied_status.id
)
)
end
end
end
end

View file

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AnnualReport::TypeDistribution do
describe '#generate' do
subject { described_class.new(account, Time.zone.now.year) }
context 'with an inactive account' do
let(:account) { Fabricate :account }
it 'builds a report for an account' do
expect(subject.generate)
.to include(
type_distribution: include(
total: 0,
reblogs: 0,
replies: 0,
standalone: 0
)
)
end
end
context 'with an active account' do
let(:account) { Fabricate :account }
before do
_other = Fabricate :status
Fabricate :status, reblog: Fabricate(:status), account: account
Fabricate :status, in_reply_to_id: Fabricate(:status).id, account: account, reply: true
Fabricate :status, account: account
end
it 'builds a report for an account' do
expect(subject.generate)
.to include(
type_distribution: include(
total: 3,
reblogs: 1,
replies: 1,
standalone: 1
)
)
end
end
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AnnualReport do
describe '#generate' do
subject { described_class.new(account, Time.zone.now.year) }
let(:account) { Fabricate :account }
it 'builds a report for an account' do
expect { subject.generate }
.to change(GeneratedAnnualReport, :count).by(1)
end
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe CacheBuster do
RSpec.describe CacheBuster do
subject { described_class.new(secret_header: secret_header, secret: secret, http_method: http_method) }
let(:secret_header) { nil }

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe ConnectionPool::SharedConnectionPool do
RSpec.describe ConnectionPool::SharedConnectionPool do
subject { described_class.new(size: 5, timeout: 5) { |site| mini_connection_class.new(site) } }
let(:mini_connection_class) do

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe ConnectionPool::SharedTimedStack do
RSpec.describe ConnectionPool::SharedTimedStack do
subject { described_class.new(5) { |site| mini_connection_class.new(site) } }
let(:mini_connection_class) do

View file

@ -0,0 +1,141 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ContentSecurityPolicy do
subject { described_class.new }
around do |example|
original_asset_host = Rails.configuration.action_controller.asset_host
original_web_domain = Rails.configuration.x.web_domain
original_use_https = Rails.configuration.x.use_https
example.run
Rails.configuration.action_controller.asset_host = original_asset_host
Rails.configuration.x.web_domain = original_web_domain
Rails.configuration.x.use_https = original_use_https
end
describe '#base_host' do
before { Rails.configuration.x.web_domain = 'host.example' }
it 'returns the configured value for the web domain' do
expect(subject.base_host).to eq 'host.example'
end
end
describe '#assets_host' do
context 'when asset_host is not configured' do
before { Rails.configuration.action_controller.asset_host = nil }
context 'with a configured web domain' do
before { Rails.configuration.x.web_domain = 'host.example' }
context 'when use_https is enabled' do
before { Rails.configuration.x.use_https = true }
it 'returns value from base host with https protocol' do
expect(subject.assets_host).to eq 'https://host.example'
end
end
context 'when use_https is disabled' do
before { Rails.configuration.x.use_https = false }
it 'returns value from base host with http protocol' do
expect(subject.assets_host).to eq 'http://host.example'
end
end
end
end
context 'when asset_host is configured' do
before do
Rails.configuration.action_controller.asset_host = 'https://assets.host.example'
end
it 'returns full value from configured host' do
expect(subject.assets_host).to eq 'https://assets.host.example'
end
end
end
describe '#media_hosts' do
context 'when there is no configured CDN' do
it 'defaults to using the assets_host value' do
expect(subject.media_hosts).to contain_exactly(subject.assets_host)
end
end
context 'when an S3 alias host is configured' do
around do |example|
ClimateControl.modify S3_ALIAS_HOST: 'asset-host.s3-alias.example' do
example.run
end
end
it 'uses the s3 alias host value' do
expect(subject.media_hosts).to contain_exactly(subject.assets_host, 'https://asset-host.s3-alias.example')
end
end
context 'when an S3 alias host with a trailing path is configured' do
around do |example|
ClimateControl.modify S3_ALIAS_HOST: 'asset-host.s3-alias.example/pathname' do
example.run
end
end
it 'uses the s3 alias host value and preserves the path' do
expect(subject.media_hosts).to contain_exactly(subject.assets_host, 'https://asset-host.s3-alias.example/pathname/')
end
end
context 'when an S3 cloudfront host is configured' do
around do |example|
ClimateControl.modify S3_CLOUDFRONT_HOST: 'asset-host.s3-cloudfront.example' do
example.run
end
end
it 'uses the s3 cloudfront host value' do
expect(subject.media_hosts).to contain_exactly(subject.assets_host, 'https://asset-host.s3-cloudfront.example')
end
end
context 'when an azure alias host is configured' do
around do |example|
ClimateControl.modify AZURE_ALIAS_HOST: 'asset-host.azure-alias.example' do
example.run
end
end
it 'uses the azure alias host value' do
expect(subject.media_hosts).to contain_exactly(subject.assets_host, 'https://asset-host.azure-alias.example')
end
end
context 'when s3_enabled is configured' do
around do |example|
ClimateControl.modify S3_ENABLED: 'true', S3_HOSTNAME: 'asset-host.s3.example' do
example.run
end
end
it 'uses the s3 hostname host value' do
expect(subject.media_hosts).to contain_exactly(subject.assets_host, 'https://asset-host.s3.example')
end
end
context 'when PAPERCLIP_ROOT_URL is configured' do
around do |example|
ClimateControl.modify PAPERCLIP_ROOT_URL: 'https://paperclip-host.example' do
example.run
end
end
it 'uses the provided URL in the content security policy' do
expect(subject.media_hosts).to contain_exactly(subject.assets_host, 'https://paperclip-host.example')
end
end
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe DeliveryFailureTracker do
RSpec.describe DeliveryFailureTracker do
subject { described_class.new('http://example.com/inbox') }
describe '#track_success!' do

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Extractor do
RSpec.describe Extractor do
describe 'extract_mentions_or_lists_with_indices' do
it 'returns an empty array if the given string does not have at signs' do
text = 'a string without at signs'
@ -69,10 +69,10 @@ describe Extractor do
end
end
describe 'extract_cashtags_with_indices' do
it 'returns []' do
describe 'extract_entities_with_indices' do
it 'returns empty array when cashtag present' do
text = '$cashtag'
extracted = described_class.extract_cashtags_with_indices(text)
extracted = described_class.extract_entities_with_indices(text)
expect(extracted).to eq []
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe FastIpMap do
RSpec.describe FastIpMap do
describe '#include?' do
subject { described_class.new([IPAddr.new('20.4.0.0/16'), IPAddr.new('145.22.30.0/24'), IPAddr.new('189.45.86.3')]) }

View file

@ -10,8 +10,8 @@ RSpec.describe FeedManager do
end
end
it 'tracks at least as many statuses as reblogs', skip_stub: true do
expect(FeedManager::REBLOG_FALLOFF).to be <= FeedManager::MAX_ITEMS
it 'tracks at least as many statuses as reblogs', :skip_stub do
expect(described_class::REBLOG_FALLOFF).to be <= described_class::MAX_ITEMS
end
describe '#key' do
@ -206,13 +206,13 @@ RSpec.describe FeedManager do
expect(described_class.instance.filter?(:mentions, reply, bob)).to be true
end
it 'returns true for status by silenced account who recipient is not following' do
it 'returns false for status by limited account who recipient is not following' do
status = Fabricate(:status, text: 'Hello world', account: alice)
alice.silence!
expect(described_class.instance.filter?(:mentions, status, bob)).to be true
expect(described_class.instance.filter?(:mentions, status, bob)).to be false
end
it 'returns false for status by followed silenced account' do
it 'returns false for status by followed limited account' do
status = Fabricate(:status, text: 'Hello world', account: alice)
alice.silence!
bob.follow!(alice)
@ -225,12 +225,12 @@ RSpec.describe FeedManager do
it 'trims timelines if they will have more than FeedManager::MAX_ITEMS' do
account = Fabricate(:account)
status = Fabricate(:status)
members = Array.new(FeedManager::MAX_ITEMS) { |count| [count, count] }
members = Array.new(described_class::MAX_ITEMS) { |count| [count, count] }
redis.zadd("feed:home:#{account.id}", members)
described_class.instance.push_to_home(account, status)
expect(redis.zcard("feed:home:#{account.id}")).to eq FeedManager::MAX_ITEMS
expect(redis.zcard("feed:home:#{account.id}")).to eq described_class::MAX_ITEMS
end
context 'with reblogs' do
@ -260,7 +260,7 @@ RSpec.describe FeedManager do
described_class.instance.push_to_home(account, reblogged)
# Fill the feed with intervening statuses
FeedManager::REBLOG_FALLOFF.times do
described_class::REBLOG_FALLOFF.times do
described_class.instance.push_to_home(account, Fabricate(:status))
end
@ -321,7 +321,7 @@ RSpec.describe FeedManager do
described_class.instance.push_to_home(account, reblogs.first)
# Fill the feed with intervening statuses
FeedManager::REBLOG_FALLOFF.times do
described_class::REBLOG_FALLOFF.times do
described_class.instance.push_to_home(account, Fabricate(:status))
end
@ -467,7 +467,7 @@ RSpec.describe FeedManager do
status = Fabricate(:status, reblog: reblogged)
described_class.instance.push_to_home(receiver, reblogged)
FeedManager::REBLOG_FALLOFF.times { described_class.instance.push_to_home(receiver, Fabricate(:status)) }
described_class::REBLOG_FALLOFF.times { described_class.instance.push_to_home(receiver, Fabricate(:status)) }
described_class.instance.push_to_home(receiver, status)
# The reblogging status should show up under normal conditions.

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe HashtagNormalizer do
RSpec.describe HashtagNormalizer do
subject { described_class.new }
describe '#normalize' do

View file

@ -41,6 +41,14 @@ RSpec.describe HtmlAwareFormatter do
expect(subject).to_not include 'status__content__spoiler-link'
end
end
context 'when given text containing ruby tags for east-asian languages' do
let(:text) { '<ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby>' }
it 'keeps the ruby tags' do
expect(subject).to eq '<ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby>'
end
end
end
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Importer::AccountsIndexImporter do
RSpec.describe Importer::AccountsIndexImporter do
describe 'import!' do
let(:pool) { Concurrent::FixedThreadPool.new(5) }
let(:importer) { described_class.new(batch_size: 123, executor: pool) }

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Importer::BaseImporter do
RSpec.describe Importer::BaseImporter do
describe 'import!' do
let(:pool) { Concurrent::FixedThreadPool.new(5) }
let(:importer) { described_class.new(batch_size: 123, executor: pool) }

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Importer::PublicStatusesIndexImporter do
RSpec.describe Importer::PublicStatusesIndexImporter do
describe 'import!' do
let(:pool) { Concurrent::FixedThreadPool.new(5) }
let(:importer) { described_class.new(batch_size: 123, executor: pool) }

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Importer::StatusesIndexImporter do
RSpec.describe Importer::StatusesIndexImporter do
describe 'import!' do
let(:pool) { Concurrent::FixedThreadPool.new(5) }
let(:importer) { described_class.new(batch_size: 123, executor: pool) }

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe Importer::TagsIndexImporter do
RSpec.describe Importer::TagsIndexImporter do
describe 'import!' do
let(:pool) { Concurrent::FixedThreadPool.new(5) }
let(:importer) { described_class.new(batch_size: 123, executor: pool) }

View file

@ -33,6 +33,14 @@ RSpec.describe LinkDetailsExtractor do
expect(subject.canonical_url).to eq original_url
end
end
context 'when canonical URL is set to "undefined"' do
let(:url) { 'undefined' }
it 'ignores the canonical URLs' do
expect(subject.canonical_url).to eq original_url
end
end
end
context 'when only basic metadata is present' do
@ -46,22 +54,13 @@ RSpec.describe LinkDetailsExtractor do
</html>
HTML
describe '#title' do
it 'returns the title from title tag' do
expect(subject.title).to eq 'Man bites dog'
end
end
describe '#description' do
it 'returns the description from meta tag' do
expect(subject.description).to eq "A dog's tale"
end
end
describe '#language' do
it 'returns the language from lang attribute' do
expect(subject.language).to eq 'en'
end
it 'extracts the expected values from html metadata' do
expect(subject)
.to have_attributes(
title: eq('Man bites dog'),
description: eq("A dog's tale"),
language: eq('en')
)
end
end
@ -90,34 +89,16 @@ RSpec.describe LinkDetailsExtractor do
end
shared_examples 'structured data' do
describe '#title' do
it 'returns the title from structured data' do
expect(subject.title).to eq 'Man bites dog'
end
end
describe '#description' do
it 'returns the description from structured data' do
expect(subject.description).to eq "A dog's tale"
end
end
describe '#published_at' do
it 'returns the publicaton time from structured data' do
expect(subject.published_at).to eq '2022-01-31T19:53:00+00:00'
end
end
describe '#author_name' do
it 'returns the author name from structured data' do
expect(subject.author_name).to eq 'Charlie Brown'
end
end
describe '#provider_name' do
it 'returns the provider name from structured data' do
expect(subject.provider_name).to eq 'Pet News'
end
it 'extracts the expected values from structured data' do
expect(subject)
.to have_attributes(
title: eq('Man bites dog'),
description: eq("A dog's tale"),
published_at: eq('2022-01-31T19:53:00+00:00'),
author_name: eq('Charlie Brown'),
provider_name: eq('Pet News'),
language: eq('en')
)
end
describe '#language' do
@ -162,6 +143,24 @@ RSpec.describe LinkDetailsExtractor do
include_examples 'structured data'
end
context 'with the first tag is null' do
let(:html) { <<~HTML }
<!doctype html>
<html>
<body>
<script type="application/ld+json">
null
</script>
<script type="application/ld+json">
#{ld_json}
</script>
</body>
</html>
HTML
include_examples 'structured data'
end
context 'with preceding block of unsupported LD+JSON' do
let(:html) { <<~HTML }
<!doctype html>
@ -225,6 +224,35 @@ RSpec.describe LinkDetailsExtractor do
include_examples 'structured data'
end
context 'with author names as array' do
let(:ld_json) do
{
'@context' => 'https://schema.org',
'@type' => 'NewsArticle',
'headline' => 'A lot of authors',
'description' => 'But we decided to cram them into one',
'author' => {
'@type' => 'Person',
'name' => ['Author 1', 'Author 2'],
},
}.to_json
end
let(:html) { <<~HTML }
<!doctype html>
<html>
<body>
<script type="application/ld+json">
#{ld_json}
</script>
</body>
</html>
HTML
it 'joins author names' do
expect(subject.author_name).to eq 'Author 1, Author 2'
end
end
end
context 'when Open Graph protocol data is present' do
@ -245,58 +273,19 @@ RSpec.describe LinkDetailsExtractor do
</html>
HTML
describe '#canonical_url' do
it 'returns the URL from Open Graph protocol data' do
expect(subject.canonical_url).to eq 'https://example.com/dog.html'
end
end
describe '#title' do
it 'returns the title from Open Graph protocol data' do
expect(subject.title).to eq 'Man bites dog'
end
end
describe '#description' do
it 'returns the description from Open Graph protocol data' do
expect(subject.description).to eq "A dog's tale"
end
end
describe '#published_at' do
it 'returns the publicaton time from Open Graph protocol data' do
expect(subject.published_at).to eq '2022-01-31T19:53:00+00:00'
end
end
describe '#author_name' do
it 'returns the author name from Open Graph protocol data' do
expect(subject.author_name).to eq 'Charlie Brown'
end
end
describe '#language' do
it 'returns the language from Open Graph protocol data' do
expect(subject.language).to eq 'en'
end
end
describe '#image' do
it 'returns the image from Open Graph protocol data' do
expect(subject.image).to eq 'https://example.com/snoopy.jpg'
end
end
describe '#image:alt' do
it 'returns the image description from Open Graph protocol data' do
expect(subject.image_alt).to eq 'A good boy'
end
end
describe '#provider_name' do
it 'returns the provider name from Open Graph protocol data' do
expect(subject.provider_name).to eq 'Pet News'
end
it 'extracts the expected values from open graph data' do
expect(subject)
.to have_attributes(
canonical_url: eq('https://example.com/dog.html'),
title: eq('Man bites dog'),
description: eq("A dog's tale"),
published_at: eq('2022-01-31T19:53:00+00:00'),
author_name: eq('Charlie Brown'),
language: eq('en'),
image: eq('https://example.com/snoopy.jpg'),
image_alt: eq('A good boy'),
provider_name: eq('Pet News')
)
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -3,27 +3,30 @@
require 'rails_helper'
require 'mastodon/cli/cache'
describe Mastodon::CLI::Cache do
let(:cli) { described_class.new }
RSpec.describe Mastodon::CLI::Cache do
subject { cli.invoke(action, arguments, options) }
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
end
end
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#clear' do
let(:action) { :clear }
before { allow(Rails.cache).to receive(:clear) }
it 'clears the Rails cache' do
expect { cli.invoke(:clear) }.to output(
a_string_including('OK')
).to_stdout
expect { subject }
.to output_results('OK')
expect(Rails.cache).to have_received(:clear)
end
end
describe '#recount' do
let(:action) { :recount }
context 'with the `accounts` argument' do
let(:arguments) { ['accounts'] }
let(:account_stat) { Fabricate(:account_stat) }
@ -33,9 +36,8 @@ describe Mastodon::CLI::Cache do
end
it 're-calculates account records in the cache' do
expect { cli.invoke(:recount, arguments) }.to output(
a_string_including('OK')
).to_stdout
expect { subject }
.to output_results('OK')
expect(account_stat.reload.statuses_count).to be_zero
end
@ -50,9 +52,8 @@ describe Mastodon::CLI::Cache do
end
it 're-calculates account records in the cache' do
expect { cli.invoke(:recount, arguments) }.to output(
a_string_including('OK')
).to_stdout
expect { subject }
.to output_results('OK')
expect(status_stat.reload.replies_count).to be_zero
end
@ -62,9 +63,8 @@ describe Mastodon::CLI::Cache do
let(:arguments) { ['other-type'] }
it 'Exits with an error message' do
expect { cli.invoke(:recount, arguments) }.to output(
a_string_including('Unknown')
).to_stdout.and raise_error(SystemExit)
expect { subject }
.to raise_error(Thor::Error, /Unknown/)
end
end
end

View file

@ -3,47 +3,46 @@
require 'rails_helper'
require 'mastodon/cli/canonical_email_blocks'
describe Mastodon::CLI::CanonicalEmailBlocks do
let(:cli) { described_class.new }
RSpec.describe Mastodon::CLI::CanonicalEmailBlocks do
subject { cli.invoke(action, arguments, options) }
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
end
end
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#find' do
let(:action) { :find }
let(:arguments) { ['user@example.com'] }
context 'when a block is present' do
before { Fabricate(:canonical_email_block, email: 'user@example.com') }
it 'announces the presence of the block' do
expect { cli.invoke(:find, arguments) }.to output(
a_string_including('user@example.com is blocked')
).to_stdout
expect { subject }
.to output_results('user@example.com is blocked')
end
end
context 'when a block is not present' do
it 'announces the absence of the block' do
expect { cli.invoke(:find, arguments) }.to output(
a_string_including('user@example.com is not blocked')
).to_stdout
expect { subject }
.to output_results('user@example.com is not blocked')
end
end
end
describe '#remove' do
let(:action) { :remove }
let(:arguments) { ['user@example.com'] }
context 'when a block is present' do
before { Fabricate(:canonical_email_block, email: 'user@example.com') }
it 'removes the block' do
expect { cli.invoke(:remove, arguments) }.to output(
a_string_including('Unblocked user@example.com')
).to_stdout
expect { subject }
.to output_results('Unblocked user@example.com')
expect(CanonicalEmailBlock.matching_email('user@example.com')).to be_empty
end
@ -51,9 +50,8 @@ describe Mastodon::CLI::CanonicalEmailBlocks do
context 'when a block is not present' do
it 'announces the absence of the block' do
expect { cli.invoke(:remove, arguments) }.to output(
a_string_including('user@example.com is not blocked')
).to_stdout
expect { subject }
.to output_results('user@example.com is not blocked')
end
end
end

View file

@ -3,10 +3,93 @@
require 'rails_helper'
require 'mastodon/cli/domains'
describe Mastodon::CLI::Domains do
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
RSpec.describe Mastodon::CLI::Domains do
subject { cli.invoke(action, arguments, options) }
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#purge' do
let(:action) { :purge }
context 'with invalid limited federation mode argument' do
let(:arguments) { ['example.host'] }
let(:options) { { limited_federation_mode: true } }
it 'warns about usage and exits' do
expect { subject }
.to raise_error(Thor::Error, /DOMAIN parameter not supported/)
end
end
context 'without a domains argument' do
it 'warns about usage and exits' do
expect { subject }
.to raise_error(Thor::Error, 'No domain(s) given')
end
end
context 'with accounts from the domain' do
let(:domain) { 'host.example' }
let!(:account) { Fabricate(:account, domain: domain) }
let(:arguments) { [domain] }
it 'removes the account' do
expect { subject }
.to output_results('Removed 1 accounts')
expect { account.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
describe '#crawl' do
let(:action) { :crawl }
context 'with accounts from the domain' do
let(:domain) { 'host.example' }
before do
Fabricate(:account, domain: domain)
stub_request(:get, 'https://host.example/api/v1/instance').to_return(status: 200, body: {}.to_json)
stub_request(:get, 'https://host.example/api/v1/instance/peers').to_return(status: 200, body: {}.to_json)
stub_request(:get, 'https://host.example/api/v1/instance/activity').to_return(status: 200, body: {}.to_json)
stub_const('Mastodon::CLI::Domains::CRAWL_SLEEP_TIME', 0)
end
context 'with --format of summary' do
let(:options) { { format: 'summary' } }
it 'crawls the domains and summarizes results' do
expect { subject }
.to output_results('Visited 1 domains, 0 failed')
end
end
context 'with --format of domains' do
let(:options) { { format: 'domains' } }
it 'crawls the domains and summarizes results' do
expect { subject }
.to output_results(domain)
end
end
context 'with --format of json' do
let(:options) { { format: 'json' } }
it 'crawls the domains and summarizes results' do
expect { subject }
.to output_results(json_summary)
end
def json_summary
Oj.dump('host.example': { activity: {} })
end
end
end
end
end

View file

@ -3,10 +3,100 @@
require 'rails_helper'
require 'mastodon/cli/email_domain_blocks'
describe Mastodon::CLI::EmailDomainBlocks do
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
RSpec.describe Mastodon::CLI::EmailDomainBlocks do
subject { cli.invoke(action, arguments, options) }
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#list' do
let(:action) { :list }
context 'with email domain block records' do
let!(:parent_block) { Fabricate(:email_domain_block) }
let!(:child_block) { Fabricate(:email_domain_block, parent: parent_block) }
it 'lists the blocks' do
expect { subject }
.to output_results(
parent_block.domain,
child_block.domain
)
end
end
end
describe '#add' do
let(:action) { :add }
context 'without any options' do
it 'warns about usage and exits' do
expect { subject }
.to raise_error(Thor::Error, 'No domain(s) given')
end
end
context 'when blocks exist' do
let(:options) { {} }
let(:domain) { 'host.example' }
let(:arguments) { [domain] }
before { Fabricate(:email_domain_block, domain: domain) }
it 'does not add a new block' do
expect { subject }
.to output_results('is already blocked')
.and(not_change(EmailDomainBlock, :count))
end
end
context 'when no blocks exist' do
let(:domain) { 'host.example' }
let(:arguments) { [domain] }
it 'adds a new block' do
expect { subject }
.to output_results('Added 1')
.and(change(EmailDomainBlock, :count).by(1))
end
end
end
describe '#remove' do
let(:action) { :remove }
context 'without any options' do
it 'warns about usage and exits' do
expect { subject }
.to raise_error(Thor::Error, 'No domain(s) given')
end
end
context 'when blocks exist' do
let(:domain) { 'host.example' }
let(:arguments) { [domain] }
before { Fabricate(:email_domain_block, domain: domain) }
it 'removes the block' do
expect { subject }
.to output_results('Removed 1')
.and(change(EmailDomainBlock, :count).by(-1))
end
end
context 'when no blocks exist' do
let(:domain) { 'host.example' }
let(:arguments) { [domain] }
it 'does not remove a block' do
expect { subject }
.to output_results('is not yet blocked')
.and(not_change(EmailDomainBlock, :count))
end
end
end
end

View file

@ -3,10 +3,62 @@
require 'rails_helper'
require 'mastodon/cli/emoji'
describe Mastodon::CLI::Emoji do
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
RSpec.describe Mastodon::CLI::Emoji do
subject { cli.invoke(action, arguments, options) }
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#purge' do
let(:action) { :purge }
context 'with existing custom emoji' do
before { Fabricate(:custom_emoji) }
it 'reports a successful purge' do
expect { subject }
.to output_results('OK')
end
end
end
describe '#import' do
context 'with existing custom emoji' do
let(:import_path) { Rails.root.join('spec', 'fixtures', 'files', 'elite-assets.tar.gz') }
let(:action) { :import }
let(:arguments) { [import_path] }
it 'reports about imported emoji' do
expect { subject }
.to output_results('Imported 1')
.and change(CustomEmoji, :count).by(1)
end
end
end
describe '#export' do
context 'with existing custom emoji' do
before do
FileUtils.rm_rf(export_path.dirname)
FileUtils.mkdir_p(export_path.dirname)
Fabricate(:custom_emoji)
end
after { FileUtils.rm_rf(export_path.dirname) }
let(:export_path) { Rails.root.join('tmp', 'cli-tests', 'export.tar.gz') }
let(:arguments) { [export_path.dirname.to_s] }
let(:action) { :export }
it 'reports about exported emoji' do
expect { subject }
.to output_results('Exported 1')
.and change { File.exist?(export_path) }.from(false).to(true)
end
end
end
end

View file

@ -3,25 +3,26 @@
require 'rails_helper'
require 'mastodon/cli/feeds'
describe Mastodon::CLI::Feeds do
let(:cli) { described_class.new }
RSpec.describe Mastodon::CLI::Feeds do
subject { cli.invoke(action, arguments, options) }
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
end
end
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#build' do
let(:action) { :build }
before { Fabricate(:account) }
context 'with --all option' do
let(:options) { { all: true } }
it 'regenerates feeds for all accounts' do
expect { cli.invoke(:build, [], options) }.to output(
a_string_including('Regenerated feeds')
).to_stdout
expect { subject }
.to output_results('Regenerated feeds')
end
end
@ -31,9 +32,8 @@ describe Mastodon::CLI::Feeds do
let(:arguments) { ['alice'] }
it 'regenerates feeds for the account' do
expect { cli.invoke(:build, arguments) }.to output(
a_string_including('OK')
).to_stdout
expect { subject }
.to output_results('OK')
end
end
@ -41,22 +41,22 @@ describe Mastodon::CLI::Feeds do
let(:arguments) { ['invalid-username'] }
it 'displays an error and exits' do
expect { cli.invoke(:build, arguments) }.to output(
a_string_including('No such account')
).to_stdout.and raise_error(SystemExit)
expect { subject }
.to raise_error(Thor::Error, 'No such account')
end
end
end
describe '#clear' do
let(:action) { :clear }
before do
allow(redis).to receive(:del).with(key_namespace)
end
it 'clears the redis `feed:*` namespace' do
expect { cli.invoke(:clear) }.to output(
a_string_including('OK')
).to_stdout
expect { subject }
.to output_results('OK')
expect(redis).to have_received(:del).with(key_namespace).once
end

View file

@ -3,16 +3,17 @@
require 'rails_helper'
require 'mastodon/cli/ip_blocks'
describe Mastodon::CLI::IpBlocks do
let(:cli) { described_class.new }
RSpec.describe Mastodon::CLI::IpBlocks do
subject { cli.invoke(action, arguments, options) }
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
end
end
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#add' do
let(:action) { :add }
let(:ip_list) do
[
'192.0.2.1',
@ -29,29 +30,28 @@ describe Mastodon::CLI::IpBlocks do
]
end
let(:options) { { severity: 'no_access' } }
let(:arguments) { ip_list }
shared_examples 'ip address blocking' do
it 'blocks all specified IP addresses' do
cli.invoke(:add, ip_list, options)
blocked_ip_addresses = IpBlock.where(ip: ip_list).pluck(:ip)
expected_ip_addresses = ip_list.map { |ip| IPAddr.new(ip) }
expect(blocked_ip_addresses).to match_array(expected_ip_addresses)
def blocked_ip_addresses
IpBlock.where(ip: ip_list).pluck(:ip)
end
it 'sets the severity for all blocked IP addresses' do
cli.invoke(:add, ip_list, options)
blocked_ips_severity = IpBlock.where(ip: ip_list).pluck(:severity).all?(options[:severity])
expect(blocked_ips_severity).to be(true)
def expected_ip_addresses
ip_list.map { |ip| IPAddr.new(ip) }
end
it 'displays a success message with a summary' do
expect { cli.invoke(:add, ip_list, options) }.to output(
a_string_including("Added #{ip_list.size}, skipped 0, failed 0")
).to_stdout
def blocked_ips_severity
IpBlock.where(ip: ip_list).pluck(:severity).all?(options[:severity])
end
it 'blocks and sets severity for ip address and displays summary' do
expect { subject }
.to output_results("Added #{ip_list.size}, skipped 0, failed 0")
expect(blocked_ip_addresses)
.to match_array(expected_ip_addresses)
expect(blocked_ips_severity)
.to be(true)
end
end
@ -61,28 +61,25 @@ describe Mastodon::CLI::IpBlocks do
context 'when a specified IP address is already blocked' do
let!(:blocked_ip) { IpBlock.create(ip: ip_list.last, severity: options[:severity]) }
let(:arguments) { ip_list }
it 'skips the already blocked IP address' do
allow(IpBlock).to receive(:new).and_call_original
before { allow(IpBlock).to receive(:new).and_call_original }
cli.invoke(:add, ip_list, options)
it 'skips already block ip and displays the correct summary' do
expect { subject }
.to output_results("#{ip_list.last} is already blocked\nAdded #{ip_list.size - 1}, skipped 1, failed 0")
expect(IpBlock).to_not have_received(:new).with(ip: ip_list.last)
end
it 'displays the correct summary' do
expect { cli.invoke(:add, ip_list, options) }.to output(
a_string_including("#{ip_list.last} is already blocked\nAdded #{ip_list.size - 1}, skipped 1, failed 0")
).to_stdout
end
context 'with --force option' do
let!(:blocked_ip) { IpBlock.create(ip: ip_list.last, severity: 'no_access') }
let(:options) { { severity: 'sign_up_requires_approval', force: true } }
it 'overwrites the existing IP block record' do
expect { cli.invoke(:add, ip_list, options) }
.to change { blocked_ip.reload.severity }
expect { subject }
.to output_results('Added 11')
.and change { blocked_ip.reload.severity }
.from('no_access')
.to('sign_up_requires_approval')
end
@ -93,11 +90,11 @@ describe Mastodon::CLI::IpBlocks do
context 'when a specified IP address is invalid' do
let(:ip_list) { ['320.15.175.0', '9.5.105.255', '0.0.0.0'] }
let(:arguments) { ip_list }
it 'displays the correct summary' do
expect { cli.invoke(:add, ip_list, options) }.to output(
a_string_including("#{ip_list.first} is invalid\nAdded #{ip_list.size - 1}, skipped 0, failed 1")
).to_stdout
expect { subject }
.to output_results("#{ip_list.first} is invalid\nAdded #{ip_list.size - 1}, skipped 0, failed 1")
end
end
@ -128,6 +125,7 @@ describe Mastodon::CLI::IpBlocks do
context 'when a specified IP address fails to be blocked' do
let(:ip_address) { '127.0.0.1' }
let(:ip_block) { instance_double(IpBlock, ip: ip_address, save: false) }
let(:arguments) { [ip_address] }
before do
allow(IpBlock).to receive(:new).and_return(ip_block)
@ -136,24 +134,24 @@ describe Mastodon::CLI::IpBlocks do
end
it 'displays an error message' do
expect { cli.invoke(:add, [ip_address], options) }
.to output(
a_string_including("#{ip_address} could not be saved")
).to_stdout
expect { subject }
.to output_results("#{ip_address} could not be saved")
end
end
context 'when no IP address is provided' do
let(:arguments) { [] }
it 'exits with an error message' do
expect { cli.add }.to output(
a_string_including('No IP(s) given')
).to_stdout
.and raise_error(SystemExit)
expect { subject }
.to raise_error(Thor::Error, 'No IP(s) given')
end
end
end
describe '#remove' do
let(:action) { :remove }
context 'when removing exact matches' do
let(:ip_list) do
[
@ -170,22 +168,17 @@ describe Mastodon::CLI::IpBlocks do
'::/128',
]
end
let(:arguments) { ip_list }
before do
ip_list.each { |ip| IpBlock.create(ip: ip, severity: :no_access) }
end
it 'removes exact IP blocks' do
cli.invoke(:remove, ip_list)
it 'removes exact ip blocks and displays success message with a summary' do
expect { subject }
.to output_results("Removed #{ip_list.size}, skipped 0")
expect(IpBlock.where(ip: ip_list)).to_not exist
end
it 'displays success message with a summary' do
expect { cli.invoke(:remove, ip_list) }.to output(
a_string_including("Removed #{ip_list.size}, skipped 0")
).to_stdout
end
end
context 'with --force option' do
@ -195,62 +188,60 @@ describe Mastodon::CLI::IpBlocks do
let(:arguments) { ['192.168.0.5', '10.0.1.50'] }
let(:options) { { force: true } }
it 'removes blocks for IP ranges that cover given IP(s)' do
cli.invoke(:remove, arguments, options)
it 'removes blocks for IP ranges that cover given IP(s) and keeps other ranges' do
expect { subject }
.to output_results('Removed 2')
expect(IpBlock.where(id: [first_ip_range_block.id, second_ip_range_block.id])).to_not exist
expect(covered_ranges).to_not exist
expect(other_ranges).to exist
end
it 'does not remove other IP ranges' do
cli.invoke(:remove, arguments, options)
def covered_ranges
IpBlock.where(id: [first_ip_range_block.id, second_ip_range_block.id])
end
expect(IpBlock.where(id: third_ip_range_block.id)).to exist
def other_ranges
IpBlock.where(id: third_ip_range_block.id)
end
end
context 'when a specified IP address is not blocked' do
let(:unblocked_ip) { '192.0.2.1' }
let(:arguments) { [unblocked_ip] }
it 'skips the IP address' do
expect { cli.invoke(:remove, [unblocked_ip]) }.to output(
a_string_including("#{unblocked_ip} is not yet blocked")
).to_stdout
end
it 'displays the summary correctly' do
expect { cli.invoke(:remove, [unblocked_ip]) }.to output(
a_string_including('Removed 0, skipped 1')
).to_stdout
it 'skips the IP address and displays summary' do
expect { subject }
.to output_results(
"#{unblocked_ip} is not yet blocked",
'Removed 0, skipped 1'
)
end
end
context 'when a specified IP address is invalid' do
let(:invalid_ip) { '320.15.175.0' }
let(:arguments) { [invalid_ip] }
it 'skips the invalid IP address' do
expect { cli.invoke(:remove, [invalid_ip]) }.to output(
a_string_including("#{invalid_ip} is invalid")
).to_stdout
end
it 'displays the summary correctly' do
expect { cli.invoke(:remove, [invalid_ip]) }.to output(
a_string_including('Removed 0, skipped 1')
).to_stdout
it 'skips the invalid IP address and displays summary' do
expect { subject }
.to output_results(
"#{invalid_ip} is invalid",
'Removed 0, skipped 1'
)
end
end
context 'when no IP address is provided' do
it 'exits with an error message' do
expect { cli.remove }.to output(
a_string_including('No IP(s) given')
).to_stdout
.and raise_error(SystemExit)
expect { subject }
.to raise_error(Thor::Error, 'No IP(s) given')
end
end
end
describe '#export' do
let(:action) { :export }
let(:first_ip_range_block) { IpBlock.create(ip: '192.168.0.0/24', severity: :no_access) }
let(:second_ip_range_block) { IpBlock.create(ip: '10.0.0.0/16', severity: :no_access) }
let(:third_ip_range_block) { IpBlock.create(ip: '127.0.0.1', severity: :sign_up_block) }
@ -259,15 +250,13 @@ describe Mastodon::CLI::IpBlocks do
let(:options) { { format: 'plain' } }
it 'exports blocked IPs with "no_access" severity in plain format' do
expect { cli.invoke(:export, nil, options) }.to output(
a_string_including("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}")
).to_stdout
expect { subject }
.to output_results("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}")
end
it 'does not export bloked IPs with different severities' do
expect { cli.invoke(:export, nil, options) }.to_not output(
a_string_including("#{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}")
).to_stdout
it 'does not export blocked IPs with different severities' do
expect { subject }
.to_not output_results("#{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}")
end
end
@ -275,23 +264,20 @@ describe Mastodon::CLI::IpBlocks do
let(:options) { { format: 'nginx' } }
it 'exports blocked IPs with "no_access" severity in plain format' do
expect { cli.invoke(:export, nil, options) }.to output(
a_string_including("deny #{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};\ndeny #{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix};")
).to_stdout
expect { subject }
.to output_results("deny #{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};\ndeny #{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix};")
end
it 'does not export bloked IPs with different severities' do
expect { cli.invoke(:export, nil, options) }.to_not output(
a_string_including("deny #{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};")
).to_stdout
it 'does not export blocked IPs with different severities' do
expect { subject }
.to_not output_results("deny #{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};")
end
end
context 'when --format option is not provided' do
it 'exports blocked IPs in plain format by default' do
expect { cli.export }.to output(
a_string_including("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}")
).to_stdout
expect { subject }
.to output_results("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}")
end
end
end

View file

@ -3,18 +3,174 @@
require 'rails_helper'
require 'mastodon/cli/main'
describe Mastodon::CLI::Main do
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
RSpec.describe Mastodon::CLI::Main do
subject { cli.invoke(action, arguments, options) }
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#version' do
let(:action) { :version }
it 'returns the Mastodon version' do
expect { subject }
.to output_results(Mastodon::Version.to_s)
end
end
describe 'version' do
it 'returns the Mastodon version' do
expect { described_class.new.invoke(:version) }.to output(
a_string_including(Mastodon::Version.to_s)
).to_stdout
describe '#self_destruct' do
let(:action) { :self_destruct }
context 'with self destruct mode enabled' do
before do
allow(SelfDestructHelper).to receive(:self_destruct?).and_return(true)
end
context 'with pending accounts' do
before { Fabricate(:account) }
it 'reports about pending accounts' do
expect { subject }
.to output_results(
'already enabled',
'still pending deletion'
)
.and raise_error(SystemExit)
end
end
context 'with sidekiq notices being processed' do
before do
Account.delete_all
stats_double = instance_double(Sidekiq::Stats, enqueued: 5)
allow(Sidekiq::Stats).to receive(:new).and_return(stats_double)
end
it 'reports about notices' do
expect { subject }
.to output_results(
'already enabled',
'notices are still being'
)
.and raise_error(SystemExit)
end
end
context 'with sidekiq failed deliveries' do
before do
Account.delete_all
stats_double = instance_double(Sidekiq::Stats, enqueued: 0, retry_size: 10)
allow(Sidekiq::Stats).to receive(:new).and_return(stats_double)
end
it 'reports about notices' do
expect { subject }
.to output_results(
'already enabled',
'some have failed and are scheduled'
)
.and raise_error(SystemExit)
end
end
context 'with self descruct mode ready' do
before do
Account.delete_all
stats_double = instance_double(Sidekiq::Stats, enqueued: 0, retry_size: 0)
allow(Sidekiq::Stats).to receive(:new).and_return(stats_double)
end
it 'reports about notices' do
expect { subject }
.to output_results(
'already enabled',
'can safely delete all data'
)
.and raise_error(SystemExit)
end
end
end
context 'with self destruct mode disabled' do
before do
allow(SelfDestructHelper).to receive(:self_destruct?).and_return(false)
end
context 'with an incorrect response to hostname' do
before do
answer_hostname_incorrectly
end
it 'exits with mismatch error message' do
expect { subject }
.to raise_error(Thor::Error, /Domains do not match/)
end
end
context 'with a correct response to hostname but no to proceed' do
before do
answer_hostname_correctly
decline_proceed
end
it 'passes first step but stops before instructions' do
expect { subject }
.to output_results('operation WILL NOT')
.and raise_error(Thor::Error, /Self-destruct will not begin/)
end
end
context 'with a correct response to hostname and yes to proceed' do
before do
answer_hostname_correctly
accept_proceed
end
it 'instructs to set the appropriate environment variable' do
expect { subject }
.to output_results(
'operation WILL NOT',
'the following variable'
)
end
end
private
def answer_hostname_incorrectly
allow(cli.shell)
.to receive(:ask)
.with('Type in the domain of the server to confirm:')
.and_return('wrong.host')
.once
end
def answer_hostname_correctly
allow(cli.shell)
.to receive(:ask)
.with('Type in the domain of the server to confirm:')
.and_return(Rails.configuration.x.local_domain)
.once
end
def decline_proceed
allow(cli.shell)
.to receive(:no?)
.with('Are you sure you want to proceed?')
.and_return(true)
.once
end
def accept_proceed
allow(cli.shell)
.to receive(:no?)
.with('Are you sure you want to proceed?')
.and_return(false)
.once
end
end
end
end

View file

@ -3,10 +3,601 @@
require 'rails_helper'
require 'mastodon/cli/maintenance'
describe Mastodon::CLI::Maintenance do
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
RSpec.describe Mastodon::CLI::Maintenance do
subject { cli.invoke(action, arguments, options) }
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#fix_duplicates' do
let(:action) { :fix_duplicates }
context 'when the database version is too old' do
before do
allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2000_01_01_000000) # Earlier than minimum
end
it 'Exits with error message' do
expect { subject }
.to raise_error(Thor::Error, /is too old/)
end
end
context 'when the database version is too new and the user does not continue' do
before do
allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2100_01_01_000000) # Later than maximum
allow(cli.shell).to receive(:yes?).with('Continue anyway? (Yes/No)').and_return(false).once
end
it 'Exits with error message' do
expect { subject }
.to output_results('more recent')
.and raise_error(Thor::Error, /more recent/)
end
end
context 'when Sidekiq is running' do
before do
allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2022_01_01_000000) # Higher than minimum, lower than maximum
allow(Sidekiq::ProcessSet).to receive(:new).and_return [:process]
end
it 'Exits with error message' do
expect { subject }
.to raise_error(Thor::Error, /Sidekiq is running/)
end
end
context 'when requirements are met' do
before do
allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2023_08_22_081029) # The latest migration before the cutoff
agree_to_backup_warning
end
context 'with duplicate accounts' do
before do
prepare_duplicate_data
choose_local_account_to_keep
end
let(:duplicate_account_username) { 'username' }
let(:duplicate_account_domain) { 'host.example' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating accounts',
'Multiple local accounts were found for',
'Restoring index_accounts_on_username_and_domain_lower',
'Reindexing textual indexes on accounts…',
'Finished!'
)
.and change(duplicate_remote_accounts, :count).from(2).to(1)
.and change(duplicate_local_accounts, :count).from(2).to(1)
end
def duplicate_remote_accounts
Account.where(username: duplicate_account_username, domain: duplicate_account_domain)
end
def duplicate_local_accounts
Account.where(username: duplicate_account_username, domain: nil)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :accounts, name: :index_accounts_on_username_and_domain_lower
_remote_account = Fabricate(:account, username: duplicate_account_username, domain: duplicate_account_domain)
_remote_account_dupe = Fabricate.build(:account, username: duplicate_account_username, domain: duplicate_account_domain).save(validate: false)
_local_account = Fabricate(:account, username: duplicate_account_username, domain: nil)
_local_account_dupe = Fabricate.build(:account, username: duplicate_account_username, domain: nil).save(validate: false)
end
def choose_local_account_to_keep
allow(cli.shell)
.to receive(:ask)
.with(/Account to keep unchanged/, anything)
.and_return('0')
.once
end
end
context 'with duplicate users on email' do
before do
prepare_duplicate_data
end
let(:duplicate_email) { 'duplicate@example.host' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating user records',
'Restoring users indexes',
'Finished!'
)
.and change(duplicate_users, :count).from(2).to(1)
end
def duplicate_users
User.where(email: duplicate_email)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :users, :email
Fabricate(:user, email: duplicate_email)
Fabricate.build(:user, email: duplicate_email).save(validate: false)
end
end
context 'with duplicate users on confirmation_token' do
before do
prepare_duplicate_data
end
let(:duplicate_confirmation_token) { '123ABC' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating user records',
'Unsetting confirmation token',
'Restoring users indexes',
'Finished!'
)
.and change(duplicate_users, :count).from(2).to(1)
end
def duplicate_users
User.where(confirmation_token: duplicate_confirmation_token)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :users, :confirmation_token
Fabricate(:user, confirmation_token: duplicate_confirmation_token)
Fabricate.build(:user, confirmation_token: duplicate_confirmation_token).save(validate: false)
end
end
context 'with duplicate users on reset_password_token' do
before do
prepare_duplicate_data
end
let(:duplicate_reset_password_token) { '123ABC' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating user records',
'Unsetting password reset token',
'Restoring users indexes',
'Finished!'
)
.and change(duplicate_users, :count).from(2).to(1)
end
def duplicate_users
User.where(reset_password_token: duplicate_reset_password_token)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :users, :reset_password_token
Fabricate(:user, reset_password_token: duplicate_reset_password_token)
Fabricate.build(:user, reset_password_token: duplicate_reset_password_token).save(validate: false)
end
end
context 'with duplicate account_domain_blocks' do
before do
prepare_duplicate_data
end
let(:duplicate_domain) { 'example.host' }
let(:account) { Fabricate(:account) }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Removing duplicate account domain blocks',
'Restoring account domain blocks indexes',
'Finished!'
)
.and change(duplicate_account_domain_blocks, :count).from(2).to(1)
end
def duplicate_account_domain_blocks
AccountDomainBlock.where(account: account, domain: duplicate_domain)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :account_domain_blocks, [:account_id, :domain]
Fabricate(:account_domain_block, account: account, domain: duplicate_domain)
Fabricate.build(:account_domain_block, account: account, domain: duplicate_domain).save(validate: false)
end
end
context 'with duplicate announcement_reactions' do
before do
prepare_duplicate_data
end
let(:account) { Fabricate(:account) }
let(:announcement) { Fabricate(:announcement) }
let(:name) { Fabricate(:custom_emoji).shortcode }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Removing duplicate announcement reactions',
'Restoring announcement_reactions indexes',
'Finished!'
)
.and change(duplicate_announcement_reactions, :count).from(2).to(1)
end
def duplicate_announcement_reactions
AnnouncementReaction.where(account: account, announcement: announcement, name: name)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :announcement_reactions, [:account_id, :announcement_id, :name]
Fabricate(:announcement_reaction, account: account, announcement: announcement, name: name)
Fabricate.build(:announcement_reaction, account: account, announcement: announcement, name: name).save(validate: false)
end
end
context 'with duplicate conversations' do
before do
prepare_duplicate_data
end
let(:uri) { 'https://example.host/path' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating conversations',
'Restoring conversations indexes',
'Finished!'
)
.and change(duplicate_conversations, :count).from(2).to(1)
end
def duplicate_conversations
Conversation.where(uri: uri)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :conversations, :uri
Fabricate(:conversation, uri: uri)
Fabricate.build(:conversation, uri: uri).save(validate: false)
end
end
context 'with duplicate custom_emojis' do
before do
prepare_duplicate_data
end
let(:duplicate_shortcode) { 'wowzers' }
let(:duplicate_domain) { 'example.host' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating custom_emojis',
'Restoring custom_emojis indexes',
'Finished!'
)
.and change(duplicate_custom_emojis, :count).from(2).to(1)
end
def duplicate_custom_emojis
CustomEmoji.where(shortcode: duplicate_shortcode, domain: duplicate_domain)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :custom_emojis, [:shortcode, :domain]
Fabricate(:custom_emoji, shortcode: duplicate_shortcode, domain: duplicate_domain)
Fabricate.build(:custom_emoji, shortcode: duplicate_shortcode, domain: duplicate_domain).save(validate: false)
end
end
context 'with duplicate custom_emoji_categories' do
before do
prepare_duplicate_data
end
let(:duplicate_name) { 'name_value' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating custom_emoji_categories',
'Restoring custom_emoji_categories indexes',
'Finished!'
)
.and change(duplicate_custom_emoji_categories, :count).from(2).to(1)
end
def duplicate_custom_emoji_categories
CustomEmojiCategory.where(name: duplicate_name)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :custom_emoji_categories, :name
Fabricate(:custom_emoji_category, name: duplicate_name)
Fabricate.build(:custom_emoji_category, name: duplicate_name).save(validate: false)
end
end
context 'with duplicate domain_allows' do
before do
prepare_duplicate_data
end
let(:domain) { 'example.host' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating domain_allows',
'Restoring domain_allows indexes',
'Finished!'
)
.and change(duplicate_domain_allows, :count).from(2).to(1)
end
def duplicate_domain_allows
DomainAllow.where(domain: domain)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :domain_allows, :domain
Fabricate(:domain_allow, domain: domain)
Fabricate.build(:domain_allow, domain: domain).save(validate: false)
end
end
context 'with duplicate domain_blocks' do
before do
prepare_duplicate_data
end
let(:domain) { 'example.host' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating domain_blocks',
'Restoring domain_blocks indexes',
'Finished!'
)
.and change(duplicate_domain_blocks, :count).from(2).to(1)
end
def duplicate_domain_blocks
DomainBlock.where(domain: domain)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :domain_blocks, :domain
Fabricate(:domain_block, domain: domain)
Fabricate.build(:domain_block, domain: domain).save(validate: false)
end
end
context 'with duplicate email_domain_blocks' do
before do
prepare_duplicate_data
end
let(:domain) { 'example.host' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating email_domain_blocks',
'Restoring email_domain_blocks indexes',
'Finished!'
)
.and change(duplicate_email_domain_blocks, :count).from(2).to(1)
end
def duplicate_email_domain_blocks
EmailDomainBlock.where(domain: domain)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :email_domain_blocks, :domain
Fabricate(:email_domain_block, domain: domain)
Fabricate.build(:email_domain_block, domain: domain).save(validate: false)
end
end
context 'with duplicate media_attachments' do
before do
prepare_duplicate_data
end
let(:shortcode) { 'codenam' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating media_attachments',
'Restoring media_attachments indexes',
'Finished!'
)
.and change(duplicate_media_attachments, :count).from(2).to(1)
end
def duplicate_media_attachments
MediaAttachment.where(shortcode: shortcode)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :media_attachments, :shortcode
Fabricate(:media_attachment, shortcode: shortcode)
Fabricate.build(:media_attachment, shortcode: shortcode).save(validate: false)
end
end
context 'with duplicate preview_cards' do
before do
prepare_duplicate_data
end
let(:url) { 'https://example.host/path' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating preview_cards',
'Restoring preview_cards indexes',
'Finished!'
)
.and change(duplicate_preview_cards, :count).from(2).to(1)
end
def duplicate_preview_cards
PreviewCard.where(url: url)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :preview_cards, :url
Fabricate(:preview_card, url: url)
Fabricate.build(:preview_card, url: url).save(validate: false)
end
end
context 'with duplicate statuses' do
before do
prepare_duplicate_data
end
let(:uri) { 'https://example.host/path' }
let(:account) { Fabricate(:account) }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating statuses',
'Restoring statuses indexes',
'Finished!'
)
.and change(duplicate_statuses, :count).from(2).to(1)
end
def duplicate_statuses
Status.where(uri: uri)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :statuses, :uri
Fabricate(:status, account: account, uri: uri)
duplicate = Fabricate.build(:status, account: account, uri: uri)
duplicate.save(validate: false)
Fabricate(:status_pin, account: account, status: duplicate)
Fabricate(:status, in_reply_to_id: duplicate.id)
Fabricate(:status, reblog_of_id: duplicate.id)
end
end
context 'with duplicate tags' do
before do
prepare_duplicate_data
end
let(:name) { 'tagname' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating tags',
'Restoring tags indexes',
'Finished!'
)
.and change(duplicate_tags, :count).from(2).to(1)
end
def duplicate_tags
Tag.where(name: name)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :tags, name: 'index_tags_on_name_lower_btree'
Fabricate(:tag, name: name)
Fabricate.build(:tag, name: name).save(validate: false)
end
end
context 'with duplicate webauthn_credentials' do
before do
prepare_duplicate_data
end
let(:external_id) { '123_123_123' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating webauthn_credentials',
'Restoring webauthn_credentials indexes',
'Finished!'
)
.and change(duplicate_webauthn_credentials, :count).from(2).to(1)
end
def duplicate_webauthn_credentials
WebauthnCredential.where(external_id: external_id)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :webauthn_credentials, :external_id
Fabricate(:webauthn_credential, external_id: external_id)
Fabricate.build(:webauthn_credential, external_id: external_id).save(validate: false)
end
end
context 'with duplicate webhooks' do
before do
prepare_duplicate_data
end
let(:url) { 'https://example.host/path' }
it 'runs the deduplication process' do
expect { subject }
.to output_results(
'Deduplicating webhooks',
'Restoring webhooks indexes',
'Finished!'
)
.and change(duplicate_webhooks, :count).from(2).to(1)
end
def duplicate_webhooks
Webhook.where(url: url)
end
def prepare_duplicate_data
ActiveRecord::Base.connection.remove_index :webhooks, :url
Fabricate(:webhook, url: url)
Fabricate.build(:webhook, url: url).save(validate: false)
end
end
def agree_to_backup_warning
allow(cli.shell)
.to receive(:yes?)
.with('Continue? (Yes/No)')
.and_return(true)
.once
end
end
end
end

View file

@ -3,10 +3,247 @@
require 'rails_helper'
require 'mastodon/cli/media'
describe Mastodon::CLI::Media do
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
RSpec.describe Mastodon::CLI::Media do
subject { cli.invoke(action, arguments, options) }
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#remove' do
let(:action) { :remove }
context 'with --prune-profiles and --remove-headers' do
let(:options) { { prune_profiles: true, remove_headers: true } }
it 'warns about usage and exits' do
expect { subject }
.to raise_error(Thor::Error, '--prune-profiles and --remove-headers should not be specified simultaneously')
end
end
context 'with --include-follows but not including --prune-profiles and --remove-headers' do
let(:options) { { include_follows: true } }
it 'warns about usage and exits' do
expect { subject }
.to raise_error(Thor::Error, '--include-follows can only be used with --prune-profiles or --remove-headers')
end
end
context 'with a relevant account' do
let!(:account) do
Fabricate(:account, domain: 'example.com', updated_at: 1.month.ago, last_webfingered_at: 1.month.ago, avatar: attachment_fixture('attachment.jpg'), header: attachment_fixture('attachment.jpg'))
end
context 'with --prune-profiles' do
let(:options) { { prune_profiles: true } }
it 'removes account avatars' do
expect { subject }
.to output_results('Visited 1')
expect(account.reload.avatar).to be_blank
end
end
context 'with --remove-headers' do
let(:options) { { remove_headers: true } }
it 'removes account header' do
expect { subject }
.to output_results('Visited 1')
expect(account.reload.header).to be_blank
end
end
end
context 'with a relevant media attachment' do
let!(:media_attachment) { Fabricate(:media_attachment, remote_url: 'https://example.com/image.jpg', created_at: 1.month.ago) }
context 'without options' do
it 'removes account avatars' do
expect { subject }
.to output_results('Removed 1')
expect(media_attachment.reload.file).to be_blank
expect(media_attachment.reload.thumbnail).to be_blank
end
end
end
end
describe '#usage' do
let(:action) { :usage }
context 'without options' do
it 'reports about storage size' do
expect { subject }
.to output_results('0 Bytes')
end
end
end
describe '#lookup' do
let(:action) { :lookup }
let(:arguments) { [url] }
context 'with valid url not connected to a record' do
let(:url) { 'https://example.host/assets/1' }
it 'warns about url and exits' do
expect { subject }
.to raise_error(Thor::Error, 'Not a media URL')
end
end
context 'with a valid media url' do
let(:status) { Fabricate(:status) }
let(:media_attachment) { Fabricate(:media_attachment, status: status) }
let(:url) { media_attachment.file.url(:original) }
it 'displays the url of a connected status' do
expect { subject }
.to output_results(status.id.to_s)
end
end
end
describe '#refresh' do
let(:action) { :refresh }
context 'without any options' do
it 'warns about usage and exits' do
expect { subject }
.to raise_error(Thor::Error, /Specify the source/)
end
end
context 'with --status option' do
before do
media_attachment.update(file_file_name: nil)
end
let(:media_attachment) { Fabricate(:media_attachment, status: status, remote_url: 'https://host.example/asset.jpg') }
let(:options) { { status: status.id } }
let(:status) { Fabricate(:status) }
it 'redownloads the attachment file' do
expect { subject }
.to output_results('Downloaded 1 media')
end
end
context 'with --account option' do
context 'when the account does not exist' do
let(:options) { { account: 'not-real-user@example.host' } }
it 'warns about usage and exits' do
expect { subject }
.to raise_error(Thor::Error, 'No such account')
end
end
context 'when the account exists' do
before do
media_attachment.update(file_file_name: nil)
end
let(:media_attachment) { Fabricate(:media_attachment, account: account) }
let(:options) { { account: account.acct } }
let(:account) { Fabricate(:account) }
it 'redownloads the attachment file' do
expect { subject }
.to output_results('Downloaded 1 media')
end
end
end
context 'with --domain option' do
before do
media_attachment.update(file_file_name: nil)
end
let(:domain) { 'example.host' }
let(:media_attachment) { Fabricate(:media_attachment, account: account) }
let(:options) { { domain: domain } }
let(:account) { Fabricate(:account, domain: domain) }
it 'redownloads the attachment file' do
expect { subject }
.to output_results('Downloaded 1 media')
end
end
context 'with --days option' do
before do
Fabricate(:media_attachment, remote_url: 'https://example.com/image.jpg', id: Mastodon::Snowflake.id_at(50.days.ago))
Fabricate(:media_attachment, remote_url: 'https://example.com/image.jpg', id: Mastodon::Snowflake.id_at(5.days.ago))
Fabricate(:media_attachment, remote_url: '', id: Mastodon::Snowflake.id_at(5.days.ago))
end
let(:options) { { days: 10 } }
it 'redownloads the attachment file for the remote records more recent than the option' do
expect { subject }
.to output_results('Downloaded 1 media')
end
end
end
describe '#remove_orphans' do
let(:action) { :remove_orphans }
before do
FileUtils.mkdir_p Rails.public_path.join('system')
end
context 'without any options' do
it 'runs without error' do
expect { subject }
.to output_results('Removed', 'orphans (approx')
end
end
context 'when in azure mode' do
before do
allow(Paperclip::Attachment).to receive(:default_options).and_return(storage: :azure)
end
it 'warns about usage and exits' do
expect { subject }
.to raise_error(Thor::Error, /azure storage driver is not supported/)
end
end
context 'when in fog mode' do
before do
allow(Paperclip::Attachment).to receive(:default_options).and_return(storage: :fog)
end
it 'warns about usage and exits' do
expect { subject }
.to raise_error(Thor::Error, /fog storage driver is not supported/)
end
end
context 'when in filesystem mode' do
before do
allow(File).to receive(:delete).and_return(true)
media_attachment.delete
end
let(:media_attachment) { Fabricate(:media_attachment) }
it 'removes the unlinked files' do
expect { subject }
.to output_results('Removed', 'orphans (approx')
expect(File).to have_received(:delete).with(media_attachment.file.path)
end
end
end
end

View file

@ -3,10 +3,58 @@
require 'rails_helper'
require 'mastodon/cli/preview_cards'
describe Mastodon::CLI::PreviewCards do
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
RSpec.describe Mastodon::CLI::PreviewCards do
subject { cli.invoke(action, arguments, options) }
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#remove' do
let(:action) { :remove }
context 'with relevant preview cards' do
before do
Fabricate(:preview_card, updated_at: 10.years.ago, type: :link)
Fabricate(:preview_card, updated_at: 10.months.ago, type: :photo)
Fabricate(:preview_card, updated_at: 10.days.ago, type: :photo)
end
context 'with no arguments' do
it 'deletes thumbnails for local preview cards' do
expect { subject }
.to output_results(
'Removed 2 preview cards',
'approx. 119 KB'
)
end
end
context 'with the --link option' do
let(:options) { { link: true } }
it 'deletes thumbnails for local preview cards' do
expect { subject }
.to output_results(
'Removed 1 link-type preview cards',
'approx. 59.6 KB'
)
end
end
context 'with the --days option' do
let(:options) { { days: 365 } }
it 'deletes thumbnails for local preview cards' do
expect { subject }
.to output_results(
'Removed 1 preview cards',
'approx. 59.6 KB'
)
end
end
end
end
end

View file

@ -3,10 +3,89 @@
require 'rails_helper'
require 'mastodon/cli/search'
describe Mastodon::CLI::Search do
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
RSpec.describe Mastodon::CLI::Search do
subject { cli.invoke(action, arguments, options) }
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#deploy' do
let(:action) { :deploy }
context 'with concurrency out of range' do
let(:options) { { concurrency: -100 } }
it 'Exits with error message' do
expect { subject }
.to raise_error(Thor::Error, /this concurrency setting/)
end
end
context 'with batch size out of range' do
let(:options) { { batch_size: -100_000 } }
it 'Exits with error message' do
expect { subject }
.to raise_error(Thor::Error, /this batch_size setting/)
end
end
context 'when server communication raises an error' do
let(:options) { { reset_chewy: true } }
before { allow(Chewy::Stash::Specification).to receive(:reset!).and_raise(Elasticsearch::Transport::Transport::Errors::InternalServerError) }
it 'Exits with error message' do
expect { subject }
.to raise_error(Thor::Error, /issue connecting to the search/)
end
end
context 'without options' do
before { stub_search_indexes }
let(:indexed_count) { 1 }
let(:deleted_count) { 2 }
it 'reports about storage size' do
expect { subject }
.to output_results(
"Indexed #{described_class::INDICES.size * indexed_count} records",
"de-indexed #{described_class::INDICES.size * deleted_count}"
)
end
end
def stub_search_indexes
described_class::INDICES.each do |index|
allow(index)
.to receive_messages(
specification: instance_double(Chewy::Index::Specification, changed?: true, lock!: nil),
purge: nil
)
importer_double = importer_double_for(index)
allow(importer_double).to receive(:on_progress).and_yield([indexed_count, deleted_count])
allow("Importer::#{index}Importer".constantize)
.to receive(:new)
.and_return(importer_double)
end
end
def importer_double_for(index)
instance_double(
"Importer::#{index}Importer".constantize,
clean_up!: nil,
estimate!: 100,
import!: nil,
on_failure: nil,
# on_progress: nil,
optimize_for_import!: nil,
optimize_for_search!: nil
)
end
end
end

View file

@ -3,67 +3,58 @@
require 'rails_helper'
require 'mastodon/cli/settings'
describe Mastodon::CLI::Settings do
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
end
end
RSpec.describe Mastodon::CLI::Settings do
it_behaves_like 'CLI Command'
describe 'subcommand "registrations"' do
subject { cli.invoke(action, arguments, options) }
let(:cli) { Mastodon::CLI::Registrations.new }
let(:arguments) { [] }
let(:options) { {} }
before do
Setting.registrations_mode = nil
end
describe '#open' do
it 'changes "registrations_mode" to "open"' do
expect { cli.open }.to change(Setting, :registrations_mode).from(nil).to('open')
end
let(:action) { :open }
it 'displays success message' do
expect { cli.open }.to output(
a_string_including('OK')
).to_stdout
it 'changes "registrations_mode" to "open" and displays success' do
expect { subject }
.to change(Setting, :registrations_mode).from(nil).to('open')
.and output_results('OK')
end
end
describe '#approved' do
it 'changes "registrations_mode" to "approved"' do
expect { cli.approved }.to change(Setting, :registrations_mode).from(nil).to('approved')
end
let(:action) { :approved }
it 'displays success message' do
expect { cli.approved }.to output(
a_string_including('OK')
).to_stdout
it 'changes "registrations_mode" to "approved" and displays success' do
expect { subject }
.to change(Setting, :registrations_mode).from(nil).to('approved')
.and output_results('OK')
end
context 'with --require-reason' do
before do
cli.options = { require_reason: true }
end
let(:options) { { require_reason: true } }
it 'changes "registrations_mode" to "approved"' do
expect { cli.approved }.to change(Setting, :registrations_mode).from(nil).to('approved')
end
it 'sets "require_invite_text" to "true"' do
expect { cli.approved }.to change(Setting, :require_invite_text).from(false).to(true)
it 'changes registrations_mode and require_invite_text' do
expect { subject }
.to output_results('OK')
.and change(Setting, :registrations_mode).from(nil).to('approved')
.and change(Setting, :require_invite_text).from(false).to(true)
end
end
end
describe '#close' do
it 'changes "registrations_mode" to "none"' do
expect { cli.close }.to change(Setting, :registrations_mode).from(nil).to('none')
end
let(:action) { :close }
it 'displays success message' do
expect { cli.close }.to output(
a_string_including('OK')
).to_stdout
it 'changes "registrations_mode" to "none" and displays success' do
expect { subject }
.to change(Setting, :registrations_mode).from(nil).to('none')
.and output_results('OK')
end
end
end

View file

@ -3,10 +3,32 @@
require 'rails_helper'
require 'mastodon/cli/statuses'
describe Mastodon::CLI::Statuses do
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
RSpec.describe Mastodon::CLI::Statuses do
subject { cli.invoke(action, arguments, options) }
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#remove', use_transactional_tests: false do
let(:action) { :remove }
context 'with small batch size' do
let(:options) { { batch_size: 0 } }
it 'exits with error message' do
expect { subject }
.to raise_error(Thor::Error, /Cannot run/)
end
end
context 'with default batch size' do
it 'removes unreferenced statuses' do
expect { subject }
.to output_results('Done after')
end
end
end
end

View file

@ -3,10 +3,28 @@
require 'rails_helper'
require 'mastodon/cli/upgrade'
describe Mastodon::CLI::Upgrade do
describe '.exit_on_failure?' do
it 'returns true' do
expect(described_class.exit_on_failure?).to be true
RSpec.describe Mastodon::CLI::Upgrade do
subject { cli.invoke(action, arguments, options) }
let(:cli) { described_class.new }
let(:arguments) { [] }
let(:options) { {} }
it_behaves_like 'CLI Command'
describe '#storage_schema' do
let(:action) { :storage_schema }
context 'with records that dont need upgrading' do
before do
Fabricate(:account)
Fabricate(:media_attachment)
end
it 'does not upgrade storage for the attachments' do
expect { subject }
.to output_results('Upgraded storage schema of 0 records')
end
end
end
end

View file

@ -3,7 +3,7 @@
require 'rails_helper'
require 'mastodon/migration_warning'
describe Mastodon::MigrationWarning do
RSpec.describe Mastodon::MigrationWarning do
describe 'migration_duration_warning' do
before do
allow(migration).to receive(:valid_environment?).and_return(true)

View file

@ -0,0 +1,263 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Mastodon::RedisConfiguration do
let(:redis_environment) { described_class.new }
before do
# We use one numbered namespace per parallel test runner
# in the test env. This here should test the non-test
# behavior, so we disable it temporarily.
allow(Rails.env).to receive(:test?).and_return(false)
end
shared_examples 'setting a different driver' do
context 'when setting the `REDIS_DRIVER` variable to `ruby`' do
around do |example|
ClimateControl.modify REDIS_DRIVER: 'ruby' do
example.run
end
end
it 'sets the driver accordingly' do
expect(subject[:driver]).to eq :ruby
end
end
end
shared_examples 'setting a namespace' do
context 'when setting the `REDIS_NAMESPACE` variable' do
around do |example|
ClimateControl.modify REDIS_NAMESPACE: 'testns' do
example.run
end
end
it 'uses the value for the namespace' do
expect(subject[:namespace]).to eq 'testns'
end
end
end
shared_examples 'secondary configuration' do |prefix|
context "when no `#{prefix}_REDIS_` environment variables are present" do
it 'uses the url from the base config' do
expect(subject[:url]).to eq 'redis://localhost:6379/0'
end
context 'when the base config uses sentinel' do
around do |example|
ClimateControl.modify REDIS_SENTINELS: '192.168.0.1:3000,192.168.0.2:4000', REDIS_SENTINEL_MASTER: 'mainsentinel' do
example.run
end
end
it 'uses the sentinel configuration from base config' do
expect(subject[:url]).to eq 'redis://mainsentinel/0'
expect(subject[:name]).to eq 'mainsentinel'
expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 3000 }, { host: '192.168.0.2', port: 4000 })
end
end
end
context "when the `#{prefix}_REDIS_URL` environment variable is present" do
around do |example|
ClimateControl.modify "#{prefix}_REDIS_URL": 'redis::/user@other.example.com/4' do
example.run
end
end
it 'uses the provided URL' do
expect(subject[:url]).to eq 'redis::/user@other.example.com/4'
end
end
context 'when giving separate environment variables' do
around do |example|
ClimateControl.modify "#{prefix}_REDIS_PASSWORD": 'testpass1', "#{prefix}_REDIS_HOST": 'redis2.example.com', "#{prefix}_REDIS_PORT": '3322', "#{prefix}_REDIS_DB": '8' do
example.run
end
end
it 'constructs the url from them' do
expect(subject[:url]).to eq 'redis://:testpass1@redis2.example.com:3322/8'
end
end
end
shared_examples 'sentinel support' do |prefix = nil|
prefix = prefix ? "#{prefix}_" : ''
context 'when configuring sentinel support' do
around do |example|
ClimateControl.modify "#{prefix}REDIS_PASSWORD": 'testpass1', "#{prefix}REDIS_HOST": 'redis2.example.com', "#{prefix}REDIS_SENTINELS": '192.168.0.1:3000,192.168.0.2:4000', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do
example.run
end
end
it 'constructs the url using the sentinel master name' do
expect(subject[:url]).to eq 'redis://:testpass1@mainsentinel/0'
end
it 'uses the redis password to authenticate with sentinels' do
expect(subject[:sentinel_password]).to eq 'testpass1'
end
it 'includes the sentinel master name and list of sentinels' do
expect(subject[:name]).to eq 'mainsentinel'
expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 3000 }, { host: '192.168.0.2', port: 4000 })
end
context "when giving dedicated credentials in `#{prefix}REDIS_SENTINEL_USERNAME` and `#{prefix}REDIS_SENTINEL_PASSWORD`" do
around do |example|
ClimateControl.modify "#{prefix}REDIS_SENTINEL_USERNAME": 'sentinel_user', "#{prefix}REDIS_SENTINEL_PASSWORD": 'sentinel_pass1' do
example.run
end
end
it 'uses the credential to authenticate with sentinels' do
expect(subject[:sentinel_username]).to eq 'sentinel_user'
expect(subject[:sentinel_password]).to eq 'sentinel_pass1'
end
end
end
context 'when giving sentinels without port numbers' do
context "when no default port is given via `#{prefix}REDIS_SENTINEL_PORT`" do
around do |example|
ClimateControl.modify "#{prefix}REDIS_SENTINELS": '192.168.0.1,192.168.0.2', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do
example.run
end
end
it 'uses the default sentinel port' do
expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 26_379 }, { host: '192.168.0.2', port: 26_379 })
end
end
context 'when adding port numbers to some, but not all sentinels' do
around do |example|
ClimateControl.modify "#{prefix}REDIS_SENTINELS": '192.168.0.1:5678,192.168.0.2', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do
example.run
end
end
it 'uses the given port number when available and the default otherwise' do
expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 5678 }, { host: '192.168.0.2', port: 26_379 })
end
end
context "when a default port is given via `#{prefix}REDIS_SENTINEL_PORT`" do
around do |example|
ClimateControl.modify "#{prefix}REDIS_SENTINEL_PORT": '1234', "#{prefix}REDIS_SENTINELS": '192.168.0.1,192.168.0.2', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do
example.run
end
end
it 'uses the given port number' do
expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 1234 }, { host: '192.168.0.2', port: 1234 })
end
end
end
end
describe '#base' do
subject { redis_environment.base }
context 'when no `REDIS_` environment variables are present' do
it 'uses defaults' do
expect(subject).to eq({
url: 'redis://localhost:6379/0',
driver: :hiredis,
namespace: nil,
})
end
end
context 'when the `REDIS_URL` environment variable is present' do
around do |example|
ClimateControl.modify REDIS_URL: 'redis::/user@example.com/2' do
example.run
end
end
it 'uses the provided URL' do
expect(subject).to eq({
url: 'redis::/user@example.com/2',
driver: :hiredis,
namespace: nil,
})
end
end
context 'when giving separate environment variables' do
around do |example|
ClimateControl.modify REDIS_PASSWORD: 'testpass', REDIS_HOST: 'redis.example.com', REDIS_PORT: '3333', REDIS_DB: '3' do
example.run
end
end
it 'constructs the url from them' do
expect(subject).to eq({
url: 'redis://:testpass@redis.example.com:3333/3',
driver: :hiredis,
namespace: nil,
})
end
end
include_examples 'setting a different driver'
include_examples 'setting a namespace'
include_examples 'sentinel support'
end
describe '#sidekiq' do
subject { redis_environment.sidekiq }
include_examples 'secondary configuration', 'SIDEKIQ'
include_examples 'setting a different driver'
include_examples 'setting a namespace'
include_examples 'sentinel support', 'SIDEKIQ'
end
describe '#cache' do
subject { redis_environment.cache }
it 'includes extra configuration' do
expect(subject).to eq({
url: 'redis://localhost:6379/0',
driver: :hiredis,
namespace: 'cache',
expires_in: 10.minutes,
connect_timeout: 5,
pool: {
size: 5,
timeout: 5,
},
})
end
context 'when `REDIS_NAMESPACE` is not set' do
it 'uses the `cache` namespace' do
expect(subject[:namespace]).to eq 'cache'
end
end
context 'when setting the `REDIS_NAMESPACE` variable' do
around do |example|
ClimateControl.modify REDIS_NAMESPACE: 'testns' do
example.run
end
end
it 'attaches the `_cache` postfix to the namespace' do
expect(subject[:namespace]).to eq 'testns_cache'
end
end
include_examples 'secondary configuration', 'CACHE'
include_examples 'setting a different driver'
include_examples 'sentinel support', 'CACHE'
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe OStatus::TagManager do
RSpec.describe OStatus::TagManager do
describe '#unique_tag' do
it 'returns a unique tag' do
expect(described_class.instance.unique_tag(Time.utc(2000), 12, 'Status')).to eq 'tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status'

View file

@ -0,0 +1,93 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Paperclip::ResponseWithLimitAdapter do
subject { described_class.new(response_with_limit) }
before { stub_request(:get, url).to_return(headers: headers, body: body) }
let(:response_with_limit) { ResponseWithLimit.new(response, 50.kilobytes) }
let(:response) { Request.new(:get, url).perform(&:itself) }
let(:url) { 'https://example.com/dir/foo.png' }
let(:headers) { nil }
let(:body) { attachment_fixture('600x400.jpeg').binmode.read }
it 'writes temporary file' do
expect(subject.tempfile.read).to eq body
expect(subject.size).to eq body.bytesize
end
context 'with Content-Disposition header' do
let(:headers) { { 'Content-Disposition' => 'attachment; filename="bar.png"' } }
it 'uses filename from header' do
expect(subject.original_filename).to eq 'bar.png'
end
it 'detects MIME type from content' do
expect(subject.content_type).to eq 'image/jpeg'
end
end
context 'without Content-Disposition header' do
it 'uses filename from path' do
expect(subject.original_filename).to eq 'foo.png'
end
it 'detects MIME type from content' do
expect(subject.content_type).to eq 'image/jpeg'
end
end
context 'without filename in path' do
let(:url) { 'https://example.com/' }
it 'falls back to "data"' do
expect(subject.original_filename).to eq 'data'
end
it 'detects MIME type from content' do
expect(subject.content_type).to eq 'image/jpeg'
end
end
context 'with very long filename' do
let(:url) { 'https://example.com/abcdefghijklmnopqrstuvwxyz.0123456789' }
it 'truncates the filename' do
expect(subject.original_filename).to eq 'abcdefghijklmnopqrst.0123'
end
end
context 'when response size exceeds limit' do
context 'with Content-Length header' do
let(:headers) { { 'Content-Length' => 5.megabytes } }
it 'raises without reading the body' do
allow(response).to receive(:body).and_call_original
expect { subject }.to raise_error(Mastodon::LengthValidationError, 'Content-Length 5242880 exceeds limit of 51200')
expect(response).to_not have_received(:body)
end
end
context 'without Content-Length header' do
let(:body) { SecureRandom.random_bytes(1.megabyte) }
it 'raises while reading the body' do
expect { subject }.to raise_error(Mastodon::LengthValidationError, 'Body size exceeds limit of 51200')
expect(response.content_length).to be_nil
end
end
end
context 'when response times out' do
it 'raises' do
allow(response.body.connection).to receive(:readpartial).and_raise(HTTP::TimeoutError)
expect { subject }.to raise_error(HTTP::TimeoutError)
end
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe PermalinkRedirector do
RSpec.describe PermalinkRedirector do
let(:remote_account) { Fabricate(:account, username: 'alice', domain: 'example.com', url: 'https://example.com/@alice', id: 2) }
describe '#redirect_url' do
@ -29,5 +29,20 @@ describe PermalinkRedirector do
redirector = described_class.new('@alice/123')
expect(redirector.redirect_path).to eq 'https://example.com/status-123'
end
it 'returns path for legacy status links with a query param' do
redirector = described_class.new('statuses/123?foo=bar')
expect(redirector.redirect_path).to eq 'https://example.com/status-123'
end
it 'returns path for pretty status links with a query param' do
redirector = described_class.new('@alice/123?foo=bar')
expect(redirector.redirect_path).to eq 'https://example.com/status-123'
end
it 'returns path for deck URLs with query params' do
redirector = described_class.new('/deck/directory?local=true')
expect(redirector.redirect_path).to eq '/directory?local=true'
end
end
end

View file

@ -72,6 +72,14 @@ RSpec.describe PlainTextFormatter do
expect(subject).to eq 'Lorem ipsum'
end
end
context 'when text contains HTML ruby tags' do
let(:status) { Fabricate(:status, account: remote_account, text: '<p>Lorem <ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby> ipsum</p>') }
it 'strips the comment' do
expect(subject).to eq 'Lorem 明日 (Ashita) ipsum'
end
end
end
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe RequestPool do
RSpec.describe RequestPool do
subject { described_class.new }
describe '#with' do
@ -33,18 +33,14 @@ describe RequestPool do
subject
threads = Array.new(20) do |_i|
Thread.new do
20.times do
subject.with('http://example.com') do |http_client|
http_client.get('/').flush
end
end
multi_threaded_execution(5) do
subject.with('http://example.com') do |http_client|
http_client.get('/').flush
# Nudge scheduler to yield and exercise the full pool
sleep(0.01)
end
end
threads.map(&:join)
expect(subject.size).to be > 1
end

View file

@ -3,7 +3,7 @@
require 'rails_helper'
require 'securerandom'
describe Request do
RSpec.describe Request do
subject { described_class.new(:get, 'http://example.com') }
describe '#headers' do
@ -64,8 +64,11 @@ describe Request do
end
it 'closes underlying connection' do
expect_any_instance_of(HTTP::Client).to receive(:close)
allow(subject.send(:http_client)).to receive(:close)
expect { |block| subject.perform(&block) }.to yield_control
expect(subject.send(:http_client)).to have_received(:close)
end
it 'returns response which implements body_with_limit' do
@ -97,7 +100,7 @@ describe Request do
describe "response's body_with_limit method" do
it 'rejects body more than 1 megabyte by default' do
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes))
expect { subject.perform(&:body_with_limit) }.to raise_error Mastodon::LengthValidationError
expect { subject.perform(&:body_with_limit) }.to raise_error(Mastodon::LengthValidationError, 'Body size exceeds limit of 1048576')
end
it 'accepts body less than 1 megabyte by default' do
@ -107,17 +110,17 @@ describe Request do
it 'rejects body by given size' do
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.kilobytes))
expect { subject.perform { |response| response.body_with_limit(1.kilobyte) } }.to raise_error Mastodon::LengthValidationError
expect { subject.perform { |response| response.body_with_limit(1.kilobyte) } }.to raise_error(Mastodon::LengthValidationError, 'Body size exceeds limit of 1024')
end
it 'rejects too large chunked body' do
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Transfer-Encoding' => 'chunked' })
expect { subject.perform(&:body_with_limit) }.to raise_error Mastodon::LengthValidationError
expect { subject.perform(&:body_with_limit) }.to raise_error(Mastodon::LengthValidationError, 'Body size exceeds limit of 1048576')
end
it 'rejects too large monolithic body' do
stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Content-Length' => 2.megabytes })
expect { subject.perform(&:body_with_limit) }.to raise_error Mastodon::LengthValidationError
expect { subject.perform(&:body_with_limit) }.to raise_error(Mastodon::LengthValidationError, 'Content-Length 2097152 exceeds limit of 1048576')
end
it 'truncates large monolithic body' do

View file

@ -2,9 +2,9 @@
require 'rails_helper'
describe Sanitize::Config do
RSpec.describe Sanitize::Config do
describe '::MASTODON_STRICT' do
subject { Sanitize::Config::MASTODON_STRICT }
subject { described_class::MASTODON_STRICT }
it 'converts h1 to p strong' do
expect(Sanitize.fragment('<h1>Foo</h1>', subject)).to eq '<p><strong>Foo</strong></p>'
@ -18,6 +18,10 @@ describe Sanitize::Config do
expect(Sanitize.fragment('<p>Check out:</p><ol start="3" reversed=""><li>Foo</li><li>Bar</li></ol>', subject)).to eq '<p>Check out:</p><ol start="3" reversed=""><li>Foo</li><li>Bar</li></ol>'
end
it 'keeps ruby tags' do
expect(Sanitize.fragment('<p><ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby></p>', subject)).to eq '<p><ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby></p>'
end
it 'removes a without href' do
expect(Sanitize.fragment('<a>Test</a>', subject)).to eq 'Test'
end

View file

@ -2,22 +2,25 @@
require 'rails_helper'
describe ScopeTransformer do
RSpec.describe ScopeTransformer do
describe '#apply' do
subject { described_class.new.apply(ScopeParser.new.parse(input)) }
shared_examples 'a scope' do |namespace, term, access|
it 'parses the term' do
expect(subject.term).to eq term
it 'parses the attributes' do
expect(subject)
.to have_attributes(
term: term,
namespace: namespace,
access: access
)
end
end
it 'parses the namespace' do
expect(subject.namespace).to eq namespace
end
context 'with scope "profile"' do
let(:input) { 'profile' }
it 'parses the access' do
expect(subject.access).to eq access
end
it_behaves_like 'a scope', nil, 'profile', 'read'
end
context 'with scope "read"' do

View file

@ -3,7 +3,7 @@
require 'rails_helper'
require 'parslet/rig/rspec'
describe SearchQueryParser do
RSpec.describe SearchQueryParser do
let(:parser) { described_class.new }
context 'with term' do

View file

@ -2,12 +2,43 @@
require 'rails_helper'
describe SearchQueryTransformer do
RSpec.describe SearchQueryTransformer do
subject { described_class.new.apply(parser, current_account: account) }
let(:account) { Fabricate(:account) }
let(:parser) { SearchQueryParser.new.parse(query) }
shared_examples 'date operator' do |operator|
let(:statement_operations) { [] }
[
['2022-01-01', '2022-01-01'],
['"2022-01-01"', '2022-01-01'],
['12345678', '12345678'],
['"12345678"', '12345678'],
].each do |value, parsed|
context "with #{operator}:#{value}" do
let(:query) { "#{operator}:#{value}" }
it 'transforms clauses' do
ops = statement_operations.index_with { |_op| parsed }
expect(subject.send(:must_clauses)).to be_empty
expect(subject.send(:must_not_clauses)).to be_empty
expect(subject.send(:filter_clauses).map(&:term)).to contain_exactly(**ops, time_zone: 'UTC')
end
end
end
context "with #{operator}:\"abc\"" do
let(:query) { "#{operator}:\"abc\"" }
it 'raises an exception' do
expect { subject }.to raise_error(Mastodon::FilterValidationError, 'Invalid date abc')
end
end
end
context 'with "hello world"' do
let(:query) { 'hello world' }
@ -68,13 +99,33 @@ describe SearchQueryTransformer do
end
end
context 'with \'before:"2022-01-01 23:00"\'' do
let(:query) { 'before:"2022-01-01 23:00"' }
context 'with \'is:"foo bar"\'' do
let(:query) { 'is:"foo bar"' }
it 'transforms clauses' do
expect(subject.send(:must_clauses)).to be_empty
expect(subject.send(:must_not_clauses)).to be_empty
expect(subject.send(:filter_clauses).map(&:term)).to contain_exactly(lt: '2022-01-01 23:00', time_zone: 'UTC')
expect(subject.send(:filter_clauses).map(&:term)).to contain_exactly('foo bar')
end
end
context 'with date operators' do
context 'with "before"' do
it_behaves_like 'date operator', 'before' do
let(:statement_operations) { [:lt] }
end
end
context 'with "after"' do
it_behaves_like 'date operator', 'after' do
let(:statement_operations) { [:gt] }
end
end
context 'with "during"' do
it_behaves_like 'date operator', 'during' do
let(:statement_operations) { [:gte, :lte] }
end
end
end
end

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe SignatureParser do
describe '.parse' do
subject { described_class.parse(header) }
context 'with Signature headers conforming to draft-cavage-http-signatures-12' do
let(:header) do
# This example signature string deliberately mixes uneven spacing
# and quoting styles to ensure everything is covered
'keyId = "https://remote.domain/users/bob#main-key,",algorithm= rsa-sha256 , headers="host date digest (request-target)",signature="gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ=="' # rubocop:disable Layout/LineLength
end
it 'correctly parses the header' do
expect(subject).to eq({
'keyId' => 'https://remote.domain/users/bob#main-key,',
'algorithm' => 'rsa-sha256',
'headers' => 'host date digest (request-target)',
'signature' => 'gmhMjgMROGElJU3fpehV2acD5kMHeELi8EFP2UPHOdQ54H0r55AxIpji+J3lPe+N2qSb/4H1KXIh6f0lRu8TGSsu12OQmg5hiO8VA9flcA/mh9Lpk+qwlQZIPRqKP9xUEfqD+Z7ti5wPzDKrWAUK/7FIqWgcT/mlqB1R1MGkpMFc/q4CIs2OSNiWgA4K+Kp21oQxzC2kUuYob04gAZ7cyE/FTia5t08uv6lVYFdRsn4XNPn1MsHgFBwBMRG79ng3SyhoG4PrqBEi5q2IdLq3zfre/M6He3wlCpyO2VJNdGVoTIzeZ0Zz8jUscPV3XtWUchpGclLGSaKaq/JyNZeiYQ==', # rubocop:disable Layout/LineLength
})
end
end
context 'with a malformed Signature header' do
let(:header) { 'hello this is malformed!' }
it 'raises an error' do
expect { subject }.to raise_error(described_class::ParsingError)
end
end
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe StatusCacheHydrator do
RSpec.describe StatusCacheHydrator do
let(:status) { Fabricate(:status) }
let(:account) { Fabricate(:account) }

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe StatusFilter do
RSpec.describe StatusFilter do
describe '#filtered?' do
let(:status) { Fabricate(:status) }
@ -23,7 +23,8 @@ describe StatusFilter do
context 'when status policy does not allow show' do
it 'filters the status' do
allow_any_instance_of(StatusPolicy).to receive(:show?).and_return(false)
policy = instance_double(StatusPolicy, show?: false)
allow(StatusPolicy).to receive(:new).and_return(policy)
expect(filter).to be_filtered
end
@ -74,7 +75,8 @@ describe StatusFilter do
context 'when status policy does not allow show' do
it 'filters the status' do
allow_any_instance_of(StatusPolicy).to receive(:show?).and_return(false)
policy = instance_double(StatusPolicy, show?: false)
allow(StatusPolicy).to receive(:new).and_return(policy)
expect(filter).to be_filtered
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
describe StatusFinder do
RSpec.describe StatusFinder do
include RoutingHelper
describe '#status' do

Some files were not shown because too many files have changed in this diff Show more