Improve spec coverage for collection of workers/
classes (#27874)
This commit is contained in:
parent
0a6ec048a8
commit
155fb84141
16 changed files with 460 additions and 8 deletions
5
spec/fabricators/account_deletion_request_fabricator.rb
Normal file
5
spec/fabricators/account_deletion_request_fabricator.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:account_deletion_request) do
|
||||||
|
account
|
||||||
|
end
|
7
spec/fabricators/import_fabricator.rb
Normal file
7
spec/fabricators/import_fabricator.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:import) do
|
||||||
|
account
|
||||||
|
type :following
|
||||||
|
data { attachment_fixture('imports.txt') }
|
||||||
|
end
|
52
spec/workers/account_refresh_worker_spec.rb
Normal file
52
spec/workers/account_refresh_worker_spec.rb
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe AccountRefreshWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(ResolveAccountService, call: true) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
allow(ResolveAccountService).to receive(:new).and_return(service)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account does not exist' do
|
||||||
|
it 'returns immediately without processing' do
|
||||||
|
worker.perform(123_123_123)
|
||||||
|
|
||||||
|
expect(service).to_not have_received(:call)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account exists' do
|
||||||
|
context 'when account does not need refreshing' do
|
||||||
|
let(:account) { Fabricate(:account, last_webfingered_at: recent_webfinger_at) }
|
||||||
|
|
||||||
|
it 'returns immediately without processing' do
|
||||||
|
worker.perform(account.id)
|
||||||
|
|
||||||
|
expect(service).to_not have_received(:call)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account needs refreshing' do
|
||||||
|
let(:account) { Fabricate(:account, last_webfingered_at: outdated_webfinger_at) }
|
||||||
|
|
||||||
|
it 'schedules an account update' do
|
||||||
|
worker.perform(account.id)
|
||||||
|
|
||||||
|
expect(service).to have_received(:call)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def recent_webfinger_at
|
||||||
|
(Account::BACKGROUND_REFRESH_INTERVAL - 3.days).ago
|
||||||
|
end
|
||||||
|
|
||||||
|
def outdated_webfinger_at
|
||||||
|
(Account::BACKGROUND_REFRESH_INTERVAL + 3.days).ago
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
18
spec/workers/activitypub/post_upgrade_worker_spec.rb
Normal file
18
spec/workers/activitypub/post_upgrade_worker_spec.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe ActivityPub::PostUpgradeWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
let(:domain) { 'host.example' }
|
||||||
|
|
||||||
|
it 'updates relevant values' do
|
||||||
|
account = Fabricate(:account, domain: domain, last_webfingered_at: 1.day.ago, protocol: :ostatus)
|
||||||
|
worker.perform(domain)
|
||||||
|
|
||||||
|
expect(account.reload.last_webfingered_at).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe ActivityPub::SynchronizeFeaturedTagsCollectionWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(ActivityPub::FetchFeaturedTagsCollectionService, call: true) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
allow(ActivityPub::FetchFeaturedTagsCollectionService).to receive(:new).and_return(service)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
let(:url) { 'https://host.example' }
|
||||||
|
|
||||||
|
it 'sends the account and url to the service' do
|
||||||
|
worker.perform(account.id, url)
|
||||||
|
|
||||||
|
expect(service).to have_received(:call).with(account, url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for non-existent record' do
|
||||||
|
result = worker.perform(123_123_123, url)
|
||||||
|
|
||||||
|
expect(result).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
28
spec/workers/admin/suspension_worker_spec.rb
Normal file
28
spec/workers/admin/suspension_worker_spec.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::SuspensionWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(SuspendAccountService, call: true) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
allow(SuspendAccountService).to receive(:new).and_return(service)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
it 'sends the account to the service' do
|
||||||
|
worker.perform(account.id)
|
||||||
|
|
||||||
|
expect(service).to have_received(:call).with(account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for non-existent record' do
|
||||||
|
result = worker.perform(123_123_123)
|
||||||
|
|
||||||
|
expect(result).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
29
spec/workers/after_account_domain_block_worker_spec.rb
Normal file
29
spec/workers/after_account_domain_block_worker_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe AfterAccountDomainBlockWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(AfterBlockDomainFromAccountService, call: true) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
allow(AfterBlockDomainFromAccountService).to receive(:new).and_return(service)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
let(:domain) { 'host.example' }
|
||||||
|
|
||||||
|
it 'sends the account and domain to the service' do
|
||||||
|
worker.perform(account.id, domain)
|
||||||
|
|
||||||
|
expect(service).to have_received(:call).with(account, domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for non-existent record' do
|
||||||
|
result = worker.perform(123_123_123, domain)
|
||||||
|
|
||||||
|
expect(result).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
36
spec/workers/backup_worker_spec.rb
Normal file
36
spec/workers/backup_worker_spec.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe BackupWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(BackupService, call: true) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
allow(BackupService).to receive(:new).and_return(service)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:backup) { Fabricate(:backup) }
|
||||||
|
let!(:other_backup) { Fabricate(:backup, user: backup.user) }
|
||||||
|
|
||||||
|
it 'sends the backup to the service and removes other backups' do
|
||||||
|
expect do
|
||||||
|
worker.perform(backup.id)
|
||||||
|
end.to change(UserMailer.deliveries, :size).by(1)
|
||||||
|
|
||||||
|
expect(service).to have_received(:call).with(backup)
|
||||||
|
expect { other_backup.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sidekiq retries are exhausted' do
|
||||||
|
it 'destroys the backup' do
|
||||||
|
described_class.within_sidekiq_retries_exhausted_block({ 'args' => [backup.id] }) do
|
||||||
|
worker.perform(backup.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect { backup.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
42
spec/workers/delete_mute_worker_spec.rb
Normal file
42
spec/workers/delete_mute_worker_spec.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe DeleteMuteWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(UnmuteService, call: true) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
allow(UnmuteService).to receive(:new).and_return(service)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an expired mute' do
|
||||||
|
let(:mute) { Fabricate(:mute, expires_at: 1.day.ago) }
|
||||||
|
|
||||||
|
it 'sends the mute to the service' do
|
||||||
|
worker.perform(mute.id)
|
||||||
|
|
||||||
|
expect(service).to have_received(:call).with(mute.account, mute.target_account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an unexpired mute' do
|
||||||
|
let(:mute) { Fabricate(:mute, expires_at: 1.day.from_now) }
|
||||||
|
|
||||||
|
it 'does not send the mute to the service' do
|
||||||
|
worker.perform(mute.id)
|
||||||
|
|
||||||
|
expect(service).to_not have_received(:call)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a non-existent mute' do
|
||||||
|
it 'does not send the mute to the service' do
|
||||||
|
worker.perform(123_123_123)
|
||||||
|
|
||||||
|
expect(service).to_not have_received(:call)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,6 +8,7 @@ describe FeedInsertWorker do
|
||||||
describe 'perform' do
|
describe 'perform' do
|
||||||
let(:follower) { Fabricate(:account) }
|
let(:follower) { Fabricate(:account) }
|
||||||
let(:status) { Fabricate(:status) }
|
let(:status) { Fabricate(:status) }
|
||||||
|
let(:list) { Fabricate(:list) }
|
||||||
|
|
||||||
context 'when there are no records' do
|
context 'when there are no records' do
|
||||||
it 'skips push with missing status' do
|
it 'skips push with missing status' do
|
||||||
|
@ -42,11 +43,29 @@ describe FeedInsertWorker do
|
||||||
it 'pushes the status onto the home timeline without filter' do
|
it 'pushes the status onto the home timeline without filter' do
|
||||||
instance = instance_double(FeedManager, push_to_home: nil, filter?: false)
|
instance = instance_double(FeedManager, push_to_home: nil, filter?: false)
|
||||||
allow(FeedManager).to receive(:instance).and_return(instance)
|
allow(FeedManager).to receive(:instance).and_return(instance)
|
||||||
result = subject.perform(status.id, follower.id)
|
result = subject.perform(status.id, follower.id, :home)
|
||||||
|
|
||||||
expect(result).to be_nil
|
expect(result).to be_nil
|
||||||
expect(instance).to have_received(:push_to_home).with(follower, status, update: nil)
|
expect(instance).to have_received(:push_to_home).with(follower, status, update: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'pushes the status onto the tags timeline without filter' do
|
||||||
|
instance = instance_double(FeedManager, push_to_home: nil, filter?: false)
|
||||||
|
allow(FeedManager).to receive(:instance).and_return(instance)
|
||||||
|
result = subject.perform(status.id, follower.id, :tags)
|
||||||
|
|
||||||
|
expect(result).to be_nil
|
||||||
|
expect(instance).to have_received(:push_to_home).with(follower, status, update: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'pushes the status onto the list timeline without filter' do
|
||||||
|
instance = instance_double(FeedManager, push_to_list: nil, filter?: false)
|
||||||
|
allow(FeedManager).to receive(:instance).and_return(instance)
|
||||||
|
result = subject.perform(status.id, list.id, :list)
|
||||||
|
|
||||||
|
expect(result).to be_nil
|
||||||
|
expect(instance).to have_received(:push_to_list).with(list, status, update: nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
23
spec/workers/import_worker_spec.rb
Normal file
23
spec/workers/import_worker_spec.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe ImportWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(ImportService, call: true) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
allow(ImportService).to receive(:new).and_return(service)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:import) { Fabricate(:import) }
|
||||||
|
|
||||||
|
it 'sends the import to the service' do
|
||||||
|
worker.perform(import.id)
|
||||||
|
|
||||||
|
expect(service).to have_received(:call).with(import)
|
||||||
|
expect { import.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,12 +2,38 @@
|
||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe PostProcessMediaWorker do
|
describe PostProcessMediaWorker, :paperclip_processing do
|
||||||
let(:worker) { described_class.new }
|
let(:worker) { described_class.new }
|
||||||
|
|
||||||
describe 'perform' do
|
describe '#perform' do
|
||||||
it 'runs without error for missing record' do
|
let(:media_attachment) { Fabricate(:media_attachment) }
|
||||||
expect { worker.perform(nil) }.to_not raise_error
|
|
||||||
|
it 'reprocesses and updates the media attachment' do
|
||||||
|
worker.perform(media_attachment.id)
|
||||||
|
|
||||||
|
expect(media_attachment.processing).to eq('complete')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for non-existent record' do
|
||||||
|
result = worker.perform(123_123_123)
|
||||||
|
|
||||||
|
expect(result).to be(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sidekiq retries are exhausted' do
|
||||||
|
it 'sets state to failed' do
|
||||||
|
described_class.within_sidekiq_retries_exhausted_block({ 'args' => [media_attachment.id] }) do
|
||||||
|
worker.perform(media_attachment.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(media_attachment.reload.processing).to eq('failed')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for non-existent record' do
|
||||||
|
described_class.within_sidekiq_retries_exhausted_block({ 'args' => [123_123_123] }) do
|
||||||
|
expect(worker.perform(123_123_123)).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
38
spec/workers/publish_announcement_reaction_worker_spec.rb
Normal file
38
spec/workers/publish_announcement_reaction_worker_spec.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe PublishAnnouncementReactionWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before { Fabricate(:account, user: Fabricate(:user, current_sign_in_at: 1.hour.ago)) }
|
||||||
|
|
||||||
|
let(:announcement) { Fabricate(:announcement) }
|
||||||
|
let(:name) { 'name value' }
|
||||||
|
|
||||||
|
it 'sends the announcement and name to the service when subscribed' do
|
||||||
|
allow(redis).to receive(:exists?).and_return(true)
|
||||||
|
allow(redis).to receive(:publish)
|
||||||
|
|
||||||
|
worker.perform(announcement.id, name)
|
||||||
|
|
||||||
|
expect(redis).to have_received(:publish)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not send the announcement and name to the service when not subscribed' do
|
||||||
|
allow(redis).to receive(:exists?).and_return(false)
|
||||||
|
allow(redis).to receive(:publish)
|
||||||
|
|
||||||
|
worker.perform(announcement.id, name)
|
||||||
|
|
||||||
|
expect(redis).to_not have_received(:publish)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for non-existent record' do
|
||||||
|
result = worker.perform(123_123_123, name)
|
||||||
|
|
||||||
|
expect(result).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
28
spec/workers/removal_worker_spec.rb
Normal file
28
spec/workers/removal_worker_spec.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe RemovalWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:service) { instance_double(RemoveStatusService, call: true) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
allow(RemoveStatusService).to receive(:new).and_return(service)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
it 'sends the status to the service' do
|
||||||
|
worker.perform(status.id)
|
||||||
|
|
||||||
|
expect(service).to have_received(:call).with(status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for non-existent record' do
|
||||||
|
result = worker.perform(123_123_123)
|
||||||
|
|
||||||
|
expect(result).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
60
spec/workers/scheduler/self_destruct_scheduler_spec.rb
Normal file
60
spec/workers/scheduler/self_destruct_scheduler_spec.rb
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Scheduler::SelfDestructScheduler do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
let!(:account) { Fabricate(:account, domain: nil, suspended_at: nil) }
|
||||||
|
|
||||||
|
context 'when not in self destruct mode' do
|
||||||
|
before do
|
||||||
|
allow(SelfDestructHelper).to receive(:self_destruct?).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns without processing' do
|
||||||
|
worker.perform
|
||||||
|
|
||||||
|
expect(account.reload.suspended_at).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when in self-destruct mode' do
|
||||||
|
before do
|
||||||
|
allow(SelfDestructHelper).to receive(:self_destruct?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sidekiq is overwhelmed' do
|
||||||
|
before do
|
||||||
|
stats = instance_double(Sidekiq::Stats, enqueued: described_class::MAX_ENQUEUED**2)
|
||||||
|
allow(Sidekiq::Stats).to receive(:new).and_return(stats)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns without processing' do
|
||||||
|
worker.perform
|
||||||
|
|
||||||
|
expect(account.reload.suspended_at).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sidekiq is operational' do
|
||||||
|
it 'suspends local non-suspended accounts' do
|
||||||
|
worker.perform
|
||||||
|
|
||||||
|
expect(account.reload.suspended_at).to_not be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'suspends local suspended accounts marked for deletion' do
|
||||||
|
account.update(suspended_at: 10.days.ago)
|
||||||
|
deletion_request = Fabricate(:account_deletion_request, account: account)
|
||||||
|
|
||||||
|
worker.perform
|
||||||
|
|
||||||
|
expect(account.reload.suspended_at).to be > 1.day.ago
|
||||||
|
expect { deletion_request.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,9 +5,21 @@ require 'rails_helper'
|
||||||
describe Webhooks::DeliveryWorker do
|
describe Webhooks::DeliveryWorker do
|
||||||
let(:worker) { described_class.new }
|
let(:worker) { described_class.new }
|
||||||
|
|
||||||
describe 'perform' do
|
describe '#perform' do
|
||||||
it 'runs without error' do
|
let(:webhook) { Fabricate(:webhook) }
|
||||||
expect { worker.perform(nil, nil) }.to_not raise_error
|
|
||||||
|
it 'reprocesses and updates the webhook' do
|
||||||
|
stub_request(:post, webhook.url).to_return(status: 200, body: '')
|
||||||
|
|
||||||
|
worker.perform(webhook.id, 'body')
|
||||||
|
|
||||||
|
expect(a_request(:post, webhook.url)).to have_been_made.at_least_once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true for non-existent record' do
|
||||||
|
result = worker.perform(123_123_123, '')
|
||||||
|
|
||||||
|
expect(result).to be(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue