bastodon/spec/services/activitypub/process_status_update_service_spec.rb
Claire 8f91e304a5
Fix spurious edits and require incoming edits to be explicitly marked as such (#17918)
* Change post text edit to not be considered significant if it's identical after reformatting

* We don't need to clear previous change information anymore

* Require status edits to be explicit, except for poll tallies

* Fix tests

* Add some tests

* Add poll-related tests

* Add HTML-formatting related tests
2022-04-06 21:01:02 +02:00

468 lines
13 KiB
Ruby

require 'rails_helper'
def poll_option_json(name, votes)
{ type: 'Note', name: name, replies: { type: 'Collection', totalItems: votes } }
end
RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) }
let(:alice) { Fabricate(:account) }
let(:bob) { Fabricate(:account) }
let(:mentions) { [] }
let(:tags) { [] }
let(:media_attachments) { [] }
before do
mentions.each { |a| Fabricate(:mention, status: status, account: a) }
tags.each { |t| status.tags << t }
media_attachments.each { |m| status.media_attachments << m }
end
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Note',
summary: 'Show more',
content: 'Hello universe',
updated: '2021-09-08T22:39:25Z',
tag: [
{ type: 'Hashtag', name: 'hoge' },
{ type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) },
],
}
end
let(:json) { Oj.load(Oj.dump(payload)) }
subject { described_class.new }
describe '#call' do
it 'updates text' do
subject.call(status, json)
expect(status.reload.text).to eq 'Hello universe'
end
it 'updates content warning' do
subject.call(status, json)
expect(status.reload.spoiler_text).to eq 'Show more'
end
context 'when the changes are only in sanitized-out HTML' do
let!(:status) { Fabricate(:status, text: '<p>Hello world <a href="https://joinmastodon.org" rel="nofollow">joinmastodon.org</a></p>', account: Fabricate(:account, domain: 'example.com')) }
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Note',
updated: '2021-09-08T22:39:25Z',
content: '<p>Hello world <a href="https://joinmastodon.org" rel="noreferrer">joinmastodon.org</a></p>',
}
end
before do
subject.call(status, json)
end
it 'does not create any edits' do
expect(status.reload.edits).to be_empty
end
it 'does not mark status as edited' do
expect(status.edited?).to be false
end
end
context 'when the status has not been explicitly edited' do
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Note',
content: 'Updated text',
}
end
before do
subject.call(status, json)
end
it 'does not create any edits' do
expect(status.reload.edits).to be_empty
end
it 'does not mark status as edited' do
expect(status.reload.edited?).to be false
end
it 'does not update the text' do
expect(status.reload.text).to eq 'Hello world'
end
end
context 'when the status has not been explicitly edited and features a poll' do
let(:account) { Fabricate(:account, domain: 'example.com') }
let!(:expiration) { 10.days.from_now.utc }
let!(:status) do
Fabricate(:status,
text: 'Hello world',
account: account,
poll_attributes: {
options: %w(Foo Bar),
account: account,
multiple: false,
hide_totals: false,
expires_at: expiration
}
)
end
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://example.com/foo',
type: 'Question',
content: 'Hello world',
endTime: expiration.iso8601,
oneOf: [
poll_option_json('Foo', 4),
poll_option_json('Bar', 3),
],
}
end
before do
subject.call(status, json)
end
it 'does not create any edits' do
expect(status.reload.edits).to be_empty
end
it 'does not mark status as edited' do
expect(status.reload.edited?).to be false
end
it 'does not update the text' do
expect(status.reload.text).to eq 'Hello world'
end
it 'updates tallies' do
expect(status.poll.reload.cached_tallies).to eq [4, 3]
end
end
context 'when the status changes a poll despite being not explicitly marked as updated' do
let(:account) { Fabricate(:account, domain: 'example.com') }
let!(:expiration) { 10.days.from_now.utc }
let!(:status) do
Fabricate(:status,
text: 'Hello world',
account: account,
poll_attributes: {
options: %w(Foo Bar),
account: account,
multiple: false,
hide_totals: false,
expires_at: expiration
}
)
end
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://example.com/foo',
type: 'Question',
content: 'Hello world',
endTime: expiration.iso8601,
oneOf: [
poll_option_json('Foo', 4),
poll_option_json('Bar', 3),
poll_option_json('Baz', 3),
],
}
end
before do
subject.call(status, json)
end
it 'does not create any edits' do
expect(status.reload.edits).to be_empty
end
it 'does not mark status as edited' do
expect(status.reload.edited?).to be false
end
it 'does not update the text' do
expect(status.reload.text).to eq 'Hello world'
end
it 'does not update tallies' do
expect(status.poll.reload.cached_tallies).to eq [0, 0]
end
end
context 'when receiving an edit older than the latest processed' do
before do
status.snapshot!(at_time: status.created_at, rate_limit: false)
status.update!(text: 'Hello newer world', edited_at: Time.now.utc)
status.snapshot!(rate_limit: false)
end
it 'does not create any edits' do
expect { subject.call(status, json) }.not_to change { status.reload.edits.pluck(&:id) }
end
it 'does not update the text, spoiler_text or edited_at' do
expect { subject.call(status, json) }.not_to change { s = status.reload; [s.text, s.spoiler_text, s.edited_at] }
end
end
context 'with no changes at all' do
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Note',
content: 'Hello world',
}
end
before do
subject.call(status, json)
end
it 'does not create any edits' do
expect(status.reload.edits).to be_empty
end
it 'does not mark status as edited' do
expect(status.edited?).to be false
end
end
context 'with no changes and originally with no ordered_media_attachment_ids' do
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Note',
content: 'Hello world',
}
end
before do
status.update(ordered_media_attachment_ids: nil)
subject.call(status, json)
end
it 'does not create any edits' do
expect(status.reload.edits).to be_empty
end
it 'does not mark status as edited' do
expect(status.edited?).to be false
end
end
context 'originally without tags' do
before do
subject.call(status, json)
end
it 'updates tags' do
expect(status.tags.reload.map(&:name)).to eq %w(hoge)
end
end
context 'originally with tags' do
let(:tags) { [Fabricate(:tag, name: 'test'), Fabricate(:tag, name: 'foo')] }
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Note',
summary: 'Show more',
content: 'Hello universe',
updated: '2021-09-08T22:39:25Z',
tag: [
{ type: 'Hashtag', name: 'foo' },
],
}
end
before do
subject.call(status, json)
end
it 'updates tags' do
expect(status.tags.reload.map(&:name)).to eq %w(foo)
end
end
context 'originally without mentions' do
before do
subject.call(status, json)
end
it 'updates mentions' do
expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id]
end
end
context 'originally with mentions' do
let(:mentions) { [alice, bob] }
before do
subject.call(status, json)
end
it 'updates mentions' do
expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id]
end
end
context 'originally without media attachments' do
before do
allow(RedownloadMediaWorker).to receive(:perform_async)
subject.call(status, json)
end
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Note',
content: 'Hello universe',
updated: '2021-09-08T22:39:25Z',
attachment: [
{ type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png' },
]
}
end
it 'updates media attachments' do
media_attachment = status.reload.ordered_media_attachments.first
expect(media_attachment).to_not be_nil
expect(media_attachment.remote_url).to eq 'https://example.com/foo.png'
end
it 'queues download of media attachments' do
expect(RedownloadMediaWorker).to have_received(:perform_async)
end
it 'records media change in edit' do
expect(status.edits.reload.last.ordered_media_attachment_ids).to_not be_empty
end
end
context 'originally with media attachments' do
let(:media_attachments) { [Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png'), Fabricate(:media_attachment, remote_url: 'https://example.com/unused.png')] }
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Note',
content: 'Hello universe',
updated: '2021-09-08T22:39:25Z',
attachment: [
{ type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png', name: 'A picture' },
]
}
end
before do
allow(RedownloadMediaWorker).to receive(:perform_async)
subject.call(status, json)
end
it 'updates the existing media attachment in-place' do
media_attachment = status.media_attachments.reload.first
expect(media_attachment).to_not be_nil
expect(media_attachment.remote_url).to eq 'https://example.com/foo.png'
expect(media_attachment.description).to eq 'A picture'
end
it 'does not queue redownload for the existing media attachment' do
expect(RedownloadMediaWorker).to_not have_received(:perform_async)
end
it 'updates media attachments' do
expect(status.ordered_media_attachments.map(&:remote_url)).to eq %w(https://example.com/foo.png)
end
it 'records media change in edit' do
expect(status.edits.reload.last.ordered_media_attachment_ids).to_not be_empty
end
end
context 'originally with a poll' do
before do
poll = Fabricate(:poll, status: status)
status.update(preloadable_poll: poll)
subject.call(status, json)
end
it 'removes poll' do
expect(status.reload.poll).to eq nil
end
it 'records media change in edit' do
expect(status.edits.reload.last.poll_options).to be_nil
end
end
context 'originally without a poll' do
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Question',
content: 'Hello universe',
updated: '2021-09-08T22:39:25Z',
closed: true,
oneOf: [
{ type: 'Note', name: 'Foo' },
{ type: 'Note', name: 'Bar' },
{ type: 'Note', name: 'Baz' },
],
}
end
before do
subject.call(status, json)
end
it 'creates a poll' do
poll = status.reload.poll
expect(poll).to_not be_nil
expect(poll.options).to eq %w(Foo Bar Baz)
end
it 'records media change in edit' do
expect(status.edits.reload.last.poll_options).to eq %w(Foo Bar Baz)
end
end
it 'creates edit history' do
subject.call(status, json)
expect(status.edits.reload.map(&:text)).to eq ['Hello world', 'Hello universe']
end
it 'sets edited timestamp' do
subject.call(status, json)
expect(status.reload.edited_at.to_s).to eq '2021-09-08 22:39:25 UTC'
end
end
end