process_status_update_service_spec.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. require 'rails_helper'
  2. def poll_option_json(name, votes)
  3. { type: 'Note', name: name, replies: { type: 'Collection', totalItems: votes } }
  4. end
  5. RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
  6. let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) }
  7. let(:alice) { Fabricate(:account) }
  8. let(:bob) { Fabricate(:account) }
  9. let(:mentions) { [] }
  10. let(:tags) { [] }
  11. let(:media_attachments) { [] }
  12. before do
  13. mentions.each { |a| Fabricate(:mention, status: status, account: a) }
  14. tags.each { |t| status.tags << t }
  15. media_attachments.each { |m| status.media_attachments << m }
  16. end
  17. let(:payload) do
  18. {
  19. '@context': 'https://www.w3.org/ns/activitystreams',
  20. id: 'foo',
  21. type: 'Note',
  22. summary: 'Show more',
  23. content: 'Hello universe',
  24. updated: '2021-09-08T22:39:25Z',
  25. tag: [
  26. { type: 'Hashtag', name: 'hoge' },
  27. { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) },
  28. ],
  29. }
  30. end
  31. let(:json) { Oj.load(Oj.dump(payload)) }
  32. subject { described_class.new }
  33. describe '#call' do
  34. it 'updates text' do
  35. subject.call(status, json)
  36. expect(status.reload.text).to eq 'Hello universe'
  37. end
  38. it 'updates content warning' do
  39. subject.call(status, json)
  40. expect(status.reload.spoiler_text).to eq 'Show more'
  41. end
  42. context 'when the changes are only in sanitized-out HTML' do
  43. 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')) }
  44. let(:payload) do
  45. {
  46. '@context': 'https://www.w3.org/ns/activitystreams',
  47. id: 'foo',
  48. type: 'Note',
  49. updated: '2021-09-08T22:39:25Z',
  50. content: '<p>Hello world <a href="https://joinmastodon.org" rel="noreferrer">joinmastodon.org</a></p>',
  51. }
  52. end
  53. before do
  54. subject.call(status, json)
  55. end
  56. it 'does not create any edits' do
  57. expect(status.reload.edits).to be_empty
  58. end
  59. it 'does not mark status as edited' do
  60. expect(status.edited?).to be false
  61. end
  62. end
  63. context 'when the status has not been explicitly edited' do
  64. let(:payload) do
  65. {
  66. '@context': 'https://www.w3.org/ns/activitystreams',
  67. id: 'foo',
  68. type: 'Note',
  69. content: 'Updated text',
  70. }
  71. end
  72. before do
  73. subject.call(status, json)
  74. end
  75. it 'does not create any edits' do
  76. expect(status.reload.edits).to be_empty
  77. end
  78. it 'does not mark status as edited' do
  79. expect(status.reload.edited?).to be false
  80. end
  81. it 'does not update the text' do
  82. expect(status.reload.text).to eq 'Hello world'
  83. end
  84. end
  85. context 'when the status has not been explicitly edited and features a poll' do
  86. let(:account) { Fabricate(:account, domain: 'example.com') }
  87. let!(:expiration) { 10.days.from_now.utc }
  88. let!(:status) do
  89. Fabricate(:status,
  90. text: 'Hello world',
  91. account: account,
  92. poll_attributes: {
  93. options: %w(Foo Bar),
  94. account: account,
  95. multiple: false,
  96. hide_totals: false,
  97. expires_at: expiration
  98. }
  99. )
  100. end
  101. let(:payload) do
  102. {
  103. '@context': 'https://www.w3.org/ns/activitystreams',
  104. id: 'https://example.com/foo',
  105. type: 'Question',
  106. content: 'Hello world',
  107. endTime: expiration.iso8601,
  108. oneOf: [
  109. poll_option_json('Foo', 4),
  110. poll_option_json('Bar', 3),
  111. ],
  112. }
  113. end
  114. before do
  115. subject.call(status, json)
  116. end
  117. it 'does not create any edits' do
  118. expect(status.reload.edits).to be_empty
  119. end
  120. it 'does not mark status as edited' do
  121. expect(status.reload.edited?).to be false
  122. end
  123. it 'does not update the text' do
  124. expect(status.reload.text).to eq 'Hello world'
  125. end
  126. it 'updates tallies' do
  127. expect(status.poll.reload.cached_tallies).to eq [4, 3]
  128. end
  129. end
  130. context 'when the status changes a poll despite being not explicitly marked as updated' do
  131. let(:account) { Fabricate(:account, domain: 'example.com') }
  132. let!(:expiration) { 10.days.from_now.utc }
  133. let!(:status) do
  134. Fabricate(:status,
  135. text: 'Hello world',
  136. account: account,
  137. poll_attributes: {
  138. options: %w(Foo Bar),
  139. account: account,
  140. multiple: false,
  141. hide_totals: false,
  142. expires_at: expiration
  143. }
  144. )
  145. end
  146. let(:payload) do
  147. {
  148. '@context': 'https://www.w3.org/ns/activitystreams',
  149. id: 'https://example.com/foo',
  150. type: 'Question',
  151. content: 'Hello world',
  152. endTime: expiration.iso8601,
  153. oneOf: [
  154. poll_option_json('Foo', 4),
  155. poll_option_json('Bar', 3),
  156. poll_option_json('Baz', 3),
  157. ],
  158. }
  159. end
  160. before do
  161. subject.call(status, json)
  162. end
  163. it 'does not create any edits' do
  164. expect(status.reload.edits).to be_empty
  165. end
  166. it 'does not mark status as edited' do
  167. expect(status.reload.edited?).to be false
  168. end
  169. it 'does not update the text' do
  170. expect(status.reload.text).to eq 'Hello world'
  171. end
  172. it 'does not update tallies' do
  173. expect(status.poll.reload.cached_tallies).to eq [0, 0]
  174. end
  175. end
  176. context 'when receiving an edit older than the latest processed' do
  177. before do
  178. status.snapshot!(at_time: status.created_at, rate_limit: false)
  179. status.update!(text: 'Hello newer world', edited_at: Time.now.utc)
  180. status.snapshot!(rate_limit: false)
  181. end
  182. it 'does not create any edits' do
  183. expect { subject.call(status, json) }.not_to change { status.reload.edits.pluck(&:id) }
  184. end
  185. it 'does not update the text, spoiler_text or edited_at' do
  186. expect { subject.call(status, json) }.not_to change { s = status.reload; [s.text, s.spoiler_text, s.edited_at] }
  187. end
  188. end
  189. context 'with no changes at all' do
  190. let(:payload) do
  191. {
  192. '@context': 'https://www.w3.org/ns/activitystreams',
  193. id: 'foo',
  194. type: 'Note',
  195. content: 'Hello world',
  196. }
  197. end
  198. before do
  199. subject.call(status, json)
  200. end
  201. it 'does not create any edits' do
  202. expect(status.reload.edits).to be_empty
  203. end
  204. it 'does not mark status as edited' do
  205. expect(status.edited?).to be false
  206. end
  207. end
  208. context 'with no changes and originally with no ordered_media_attachment_ids' do
  209. let(:payload) do
  210. {
  211. '@context': 'https://www.w3.org/ns/activitystreams',
  212. id: 'foo',
  213. type: 'Note',
  214. content: 'Hello world',
  215. }
  216. end
  217. before do
  218. status.update(ordered_media_attachment_ids: nil)
  219. subject.call(status, json)
  220. end
  221. it 'does not create any edits' do
  222. expect(status.reload.edits).to be_empty
  223. end
  224. it 'does not mark status as edited' do
  225. expect(status.edited?).to be false
  226. end
  227. end
  228. context 'originally without tags' do
  229. before do
  230. subject.call(status, json)
  231. end
  232. it 'updates tags' do
  233. expect(status.tags.reload.map(&:name)).to eq %w(hoge)
  234. end
  235. end
  236. context 'originally with tags' do
  237. let(:tags) { [Fabricate(:tag, name: 'test'), Fabricate(:tag, name: 'foo')] }
  238. let(:payload) do
  239. {
  240. '@context': 'https://www.w3.org/ns/activitystreams',
  241. id: 'foo',
  242. type: 'Note',
  243. summary: 'Show more',
  244. content: 'Hello universe',
  245. updated: '2021-09-08T22:39:25Z',
  246. tag: [
  247. { type: 'Hashtag', name: 'foo' },
  248. ],
  249. }
  250. end
  251. before do
  252. subject.call(status, json)
  253. end
  254. it 'updates tags' do
  255. expect(status.tags.reload.map(&:name)).to eq %w(foo)
  256. end
  257. end
  258. context 'originally without mentions' do
  259. before do
  260. subject.call(status, json)
  261. end
  262. it 'updates mentions' do
  263. expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id]
  264. end
  265. end
  266. context 'originally with mentions' do
  267. let(:mentions) { [alice, bob] }
  268. before do
  269. subject.call(status, json)
  270. end
  271. it 'updates mentions' do
  272. expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id]
  273. end
  274. end
  275. context 'originally without media attachments' do
  276. before do
  277. allow(RedownloadMediaWorker).to receive(:perform_async)
  278. subject.call(status, json)
  279. end
  280. let(:payload) do
  281. {
  282. '@context': 'https://www.w3.org/ns/activitystreams',
  283. id: 'foo',
  284. type: 'Note',
  285. content: 'Hello universe',
  286. updated: '2021-09-08T22:39:25Z',
  287. attachment: [
  288. { type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png' },
  289. ]
  290. }
  291. end
  292. it 'updates media attachments' do
  293. media_attachment = status.reload.ordered_media_attachments.first
  294. expect(media_attachment).to_not be_nil
  295. expect(media_attachment.remote_url).to eq 'https://example.com/foo.png'
  296. end
  297. it 'queues download of media attachments' do
  298. expect(RedownloadMediaWorker).to have_received(:perform_async)
  299. end
  300. it 'records media change in edit' do
  301. expect(status.edits.reload.last.ordered_media_attachment_ids).to_not be_empty
  302. end
  303. end
  304. context 'originally with media attachments' do
  305. let(:media_attachments) { [Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png'), Fabricate(:media_attachment, remote_url: 'https://example.com/unused.png')] }
  306. let(:payload) do
  307. {
  308. '@context': 'https://www.w3.org/ns/activitystreams',
  309. id: 'foo',
  310. type: 'Note',
  311. content: 'Hello universe',
  312. updated: '2021-09-08T22:39:25Z',
  313. attachment: [
  314. { type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png', name: 'A picture' },
  315. ]
  316. }
  317. end
  318. before do
  319. allow(RedownloadMediaWorker).to receive(:perform_async)
  320. subject.call(status, json)
  321. end
  322. it 'updates the existing media attachment in-place' do
  323. media_attachment = status.media_attachments.reload.first
  324. expect(media_attachment).to_not be_nil
  325. expect(media_attachment.remote_url).to eq 'https://example.com/foo.png'
  326. expect(media_attachment.description).to eq 'A picture'
  327. end
  328. it 'does not queue redownload for the existing media attachment' do
  329. expect(RedownloadMediaWorker).to_not have_received(:perform_async)
  330. end
  331. it 'updates media attachments' do
  332. expect(status.ordered_media_attachments.map(&:remote_url)).to eq %w(https://example.com/foo.png)
  333. end
  334. it 'records media change in edit' do
  335. expect(status.edits.reload.last.ordered_media_attachment_ids).to_not be_empty
  336. end
  337. end
  338. context 'originally with a poll' do
  339. before do
  340. poll = Fabricate(:poll, status: status)
  341. status.update(preloadable_poll: poll)
  342. subject.call(status, json)
  343. end
  344. it 'removes poll' do
  345. expect(status.reload.poll).to eq nil
  346. end
  347. it 'records media change in edit' do
  348. expect(status.edits.reload.last.poll_options).to be_nil
  349. end
  350. end
  351. context 'originally without a poll' do
  352. let(:payload) do
  353. {
  354. '@context': 'https://www.w3.org/ns/activitystreams',
  355. id: 'foo',
  356. type: 'Question',
  357. content: 'Hello universe',
  358. updated: '2021-09-08T22:39:25Z',
  359. closed: true,
  360. oneOf: [
  361. { type: 'Note', name: 'Foo' },
  362. { type: 'Note', name: 'Bar' },
  363. { type: 'Note', name: 'Baz' },
  364. ],
  365. }
  366. end
  367. before do
  368. subject.call(status, json)
  369. end
  370. it 'creates a poll' do
  371. poll = status.reload.poll
  372. expect(poll).to_not be_nil
  373. expect(poll.options).to eq %w(Foo Bar Baz)
  374. end
  375. it 'records media change in edit' do
  376. expect(status.edits.reload.last.poll_options).to eq %w(Foo Bar Baz)
  377. end
  378. end
  379. it 'creates edit history' do
  380. subject.call(status, json)
  381. expect(status.edits.reload.map(&:text)).to eq ['Hello world', 'Hello universe']
  382. end
  383. it 'sets edited timestamp' do
  384. subject.call(status, json)
  385. expect(status.reload.edited_at.to_s).to eq '2021-09-08 22:39:25 UTC'
  386. end
  387. end
  388. end