You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

595 lines
19 KiB

8 years ago
Account domain blocks (#2381) * Add <ostatus:conversation /> tag to Atom input/output Only uses ref attribute (not href) because href would be the alternate link that's always included also. Creates new conversation for every non-reply status. Carries over conversation for every reply. Keeps remote URIs verbatim, generates local URIs on the fly like the rest of them. * Conversation muting - prevents notifications that reference a conversation (including replies, favourites, reblogs) from being created. API endpoints /api/v1/statuses/:id/mute and /api/v1/statuses/:id/unmute Currently no way to tell when a status/conversation is muted, so the web UI only has a "disable notifications" button, doesn't work as a toggle * Display "Dismiss notifications" on all statuses in notifications column, not just own * Add "muted" as a boolean attribute on statuses JSON For now always false on contained reblogs, since it's only relevant for statuses returned from the notifications endpoint, which are not nested Remove "Disable notifications" from detailed status view, since it's only relevant in the notifications column * Up max class length * Remove pending test for conversation mute * Add tests, clean up * Rename to "mute conversation" and "unmute conversation" * Raise validation error when trying to mute/unmute status without conversation * Adding account domain blocks that filter notifications and public timelines * Add tests for domain blocks in notifications, public timelines Filter reblogs of blocked domains from home * Add API for listing and creating account domain blocks * API for creating/deleting domain blocks, tests for Status#ancestors and Status#descendants, filter domain blocks from them * Filter domains in streaming API * Update account_domain_block_spec.rb
7 years ago
Account domain blocks (#2381) * Add <ostatus:conversation /> tag to Atom input/output Only uses ref attribute (not href) because href would be the alternate link that's always included also. Creates new conversation for every non-reply status. Carries over conversation for every reply. Keeps remote URIs verbatim, generates local URIs on the fly like the rest of them. * Conversation muting - prevents notifications that reference a conversation (including replies, favourites, reblogs) from being created. API endpoints /api/v1/statuses/:id/mute and /api/v1/statuses/:id/unmute Currently no way to tell when a status/conversation is muted, so the web UI only has a "disable notifications" button, doesn't work as a toggle * Display "Dismiss notifications" on all statuses in notifications column, not just own * Add "muted" as a boolean attribute on statuses JSON For now always false on contained reblogs, since it's only relevant for statuses returned from the notifications endpoint, which are not nested Remove "Disable notifications" from detailed status view, since it's only relevant in the notifications column * Up max class length * Remove pending test for conversation mute * Add tests, clean up * Rename to "mute conversation" and "unmute conversation" * Raise validation error when trying to mute/unmute status without conversation * Adding account domain blocks that filter notifications and public timelines * Add tests for domain blocks in notifications, public timelines Filter reblogs of blocked domains from home * Add API for listing and creating account domain blocks * API for creating/deleting domain blocks, tests for Status#ancestors and Status#descendants, filter domain blocks from them * Filter domains in streaming API * Update account_domain_block_spec.rb
7 years ago
Account domain blocks (#2381) * Add <ostatus:conversation /> tag to Atom input/output Only uses ref attribute (not href) because href would be the alternate link that's always included also. Creates new conversation for every non-reply status. Carries over conversation for every reply. Keeps remote URIs verbatim, generates local URIs on the fly like the rest of them. * Conversation muting - prevents notifications that reference a conversation (including replies, favourites, reblogs) from being created. API endpoints /api/v1/statuses/:id/mute and /api/v1/statuses/:id/unmute Currently no way to tell when a status/conversation is muted, so the web UI only has a "disable notifications" button, doesn't work as a toggle * Display "Dismiss notifications" on all statuses in notifications column, not just own * Add "muted" as a boolean attribute on statuses JSON For now always false on contained reblogs, since it's only relevant for statuses returned from the notifications endpoint, which are not nested Remove "Disable notifications" from detailed status view, since it's only relevant in the notifications column * Up max class length * Remove pending test for conversation mute * Add tests, clean up * Rename to "mute conversation" and "unmute conversation" * Raise validation error when trying to mute/unmute status without conversation * Adding account domain blocks that filter notifications and public timelines * Add tests for domain blocks in notifications, public timelines Filter reblogs of blocked domains from home * Add API for listing and creating account domain blocks * API for creating/deleting domain blocks, tests for Status#ancestors and Status#descendants, filter domain blocks from them * Filter domains in streaming API * Update account_domain_block_spec.rb
7 years ago
Account domain blocks (#2381) * Add <ostatus:conversation /> tag to Atom input/output Only uses ref attribute (not href) because href would be the alternate link that's always included also. Creates new conversation for every non-reply status. Carries over conversation for every reply. Keeps remote URIs verbatim, generates local URIs on the fly like the rest of them. * Conversation muting - prevents notifications that reference a conversation (including replies, favourites, reblogs) from being created. API endpoints /api/v1/statuses/:id/mute and /api/v1/statuses/:id/unmute Currently no way to tell when a status/conversation is muted, so the web UI only has a "disable notifications" button, doesn't work as a toggle * Display "Dismiss notifications" on all statuses in notifications column, not just own * Add "muted" as a boolean attribute on statuses JSON For now always false on contained reblogs, since it's only relevant for statuses returned from the notifications endpoint, which are not nested Remove "Disable notifications" from detailed status view, since it's only relevant in the notifications column * Up max class length * Remove pending test for conversation mute * Add tests, clean up * Rename to "mute conversation" and "unmute conversation" * Raise validation error when trying to mute/unmute status without conversation * Adding account domain blocks that filter notifications and public timelines * Add tests for domain blocks in notifications, public timelines Filter reblogs of blocked domains from home * Add API for listing and creating account domain blocks * API for creating/deleting domain blocks, tests for Status#ancestors and Status#descendants, filter domain blocks from them * Filter domains in streaming API * Update account_domain_block_spec.rb
7 years ago
8 years ago
  1. require 'rails_helper'
  2. RSpec.describe Status, type: :model do
  3. let(:alice) { Fabricate(:account, username: 'alice') }
  4. let(:bob) { Fabricate(:account, username: 'bob') }
  5. let(:other) { Fabricate(:status, account: bob, text: 'Skulls for the skull god! The enemy\'s gates are sideways!')}
  6. subject { Fabricate(:status, account: alice) }
  7. describe '#local?' do
  8. it 'returns true when no remote URI is set' do
  9. expect(subject.local?).to be true
  10. end
  11. it 'returns false if a remote URI is set' do
  12. subject.uri = 'a'
  13. expect(subject.local?).to be false
  14. end
  15. end
  16. describe '#reblog?' do
  17. it 'returns true when the status reblogs another status' do
  18. subject.reblog = other
  19. expect(subject.reblog?).to be true
  20. end
  21. it 'returns false if the status is self-contained' do
  22. expect(subject.reblog?).to be false
  23. end
  24. end
  25. describe '#reply?' do
  26. it 'returns true if the status references another' do
  27. subject.thread = other
  28. expect(subject.reply?).to be true
  29. end
  30. it 'returns false if the status is self-contained' do
  31. expect(subject.reply?).to be false
  32. end
  33. end
  34. describe '#verb' do
  35. it 'is always post' do
  36. expect(subject.verb).to be :post
  37. end
  38. end
  39. describe '#object_type' do
  40. it 'is note when the status is self-contained' do
  41. expect(subject.object_type).to be :note
  42. end
  43. it 'is comment when the status replies to another' do
  44. subject.thread = other
  45. expect(subject.object_type).to be :comment
  46. end
  47. end
  48. describe '#title' do
  49. it 'is a shorter version of the content' do
  50. expect(subject.title).to be_a String
  51. end
  52. end
  53. describe '#content' do
  54. it 'returns the text of the status if it is not a reblog' do
  55. expect(subject.content).to eql subject.text
  56. end
  57. it 'returns the text of the reblogged status' do
  58. subject.reblog = other
  59. expect(subject.content).to eql other.text
  60. end
  61. end
  62. describe '#target' do
  63. it 'returns nil if the status is self-contained' do
  64. expect(subject.target).to be_nil
  65. end
  66. it 'returns nil if the status is a reply' do
  67. subject.thread = other
  68. expect(subject.target).to be_nil
  69. end
  70. it 'returns the reblogged status' do
  71. subject.reblog = other
  72. expect(subject.target).to eq other
  73. end
  74. end
  75. describe '#reblogs_count' do
  76. it 'is the number of reblogs' do
  77. Fabricate(:status, account: bob, reblog: subject)
  78. Fabricate(:status, account: alice, reblog: subject)
  79. expect(subject.reblogs_count).to eq 2
  80. end
  81. end
  82. describe '#favourites_count' do
  83. it 'is the number of favorites' do
  84. Fabricate(:favourite, account: bob, status: subject)
  85. Fabricate(:favourite, account: alice, status: subject)
  86. expect(subject.favourites_count).to eq 2
  87. end
  88. end
  89. describe '#proper' do
  90. it 'is itself for original statuses' do
  91. expect(subject.proper).to eq subject
  92. end
  93. it 'is the source status for reblogs' do
  94. subject.reblog = other
  95. expect(subject.proper).to eq other
  96. end
  97. end
  98. describe '#ancestors' do
  99. let!(:alice) { Fabricate(:account, username: 'alice') }
  100. let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') }
  101. let!(:jeff) { Fabricate(:account, username: 'jeff') }
  102. let!(:status) { Fabricate(:status, account: alice) }
  103. let!(:reply1) { Fabricate(:status, thread: status, account: jeff) }
  104. let!(:reply2) { Fabricate(:status, thread: reply1, account: bob) }
  105. let!(:reply3) { Fabricate(:status, thread: reply2, account: alice) }
  106. let!(:viewer) { Fabricate(:account, username: 'viewer') }
  107. it 'returns conversation history' do
  108. expect(reply3.ancestors).to include(status, reply1, reply2)
  109. end
  110. it 'does not return conversation history user is not allowed to see' do
  111. reply1.update(visibility: :private)
  112. status.update(visibility: :direct)
  113. expect(reply3.ancestors(viewer)).to_not include(reply1, status)
  114. end
  115. it 'does not return conversation history from blocked users' do
  116. viewer.block!(jeff)
  117. expect(reply3.ancestors(viewer)).to_not include(reply1)
  118. end
  119. it 'does not return conversation history from muted users' do
  120. viewer.mute!(jeff)
  121. expect(reply3.ancestors(viewer)).to_not include(reply1)
  122. end
  123. it 'does not return conversation history from silenced and not followed users' do
  124. jeff.update(silenced: true)
  125. expect(reply3.ancestors(viewer)).to_not include(reply1)
  126. end
  127. it 'does not return conversation history from blocked domains' do
  128. viewer.block_domain!('example.com')
  129. expect(reply3.ancestors(viewer)).to_not include(reply2)
  130. end
  131. it 'ignores deleted records' do
  132. first_status = Fabricate(:status, account: bob)
  133. second_status = Fabricate(:status, thread: first_status, account: alice)
  134. # Create cache and delete cached record
  135. second_status.ancestors
  136. first_status.destroy
  137. expect(second_status.ancestors).to eq([])
  138. end
  139. end
  140. describe '#descendants' do
  141. let!(:alice) { Fabricate(:account, username: 'alice') }
  142. let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') }
  143. let!(:jeff) { Fabricate(:account, username: 'jeff') }
  144. let!(:status) { Fabricate(:status, account: alice) }
  145. let!(:reply1) { Fabricate(:status, thread: status, account: alice) }
  146. let!(:reply2) { Fabricate(:status, thread: status, account: bob) }
  147. let!(:reply3) { Fabricate(:status, thread: reply1, account: jeff) }
  148. let!(:viewer) { Fabricate(:account, username: 'viewer') }
  149. it 'returns replies' do
  150. expect(status.descendants).to include(reply1, reply2, reply3)
  151. end
  152. it 'does not return replies user is not allowed to see' do
  153. reply1.update(visibility: :private)
  154. reply3.update(visibility: :direct)
  155. expect(status.descendants(viewer)).to_not include(reply1, reply3)
  156. end
  157. it 'does not return replies from blocked users' do
  158. viewer.block!(jeff)
  159. expect(status.descendants(viewer)).to_not include(reply3)
  160. end
  161. it 'does not return replies from muted users' do
  162. viewer.mute!(jeff)
  163. expect(status.descendants(viewer)).to_not include(reply3)
  164. end
  165. it 'does not return replies from silenced and not followed users' do
  166. jeff.update(silenced: true)
  167. expect(status.descendants(viewer)).to_not include(reply3)
  168. end
  169. it 'does not return replies from blocked domains' do
  170. viewer.block_domain!('example.com')
  171. expect(status.descendants(viewer)).to_not include(reply2)
  172. end
  173. end
  174. describe '.mutes_map' do
  175. let(:status) { Fabricate(:status) }
  176. let(:account) { Fabricate(:account) }
  177. subject { Status.mutes_map([status.conversation.id], account) }
  178. it 'returns a hash' do
  179. expect(subject).to be_a Hash
  180. end
  181. it 'contains true value' do
  182. account.mute_conversation!(status.conversation)
  183. expect(subject[status.conversation.id]).to be true
  184. end
  185. end
  186. describe '.favourites_map' do
  187. let(:status) { Fabricate(:status) }
  188. let(:account) { Fabricate(:account) }
  189. subject { Status.favourites_map([status], account) }
  190. it 'returns a hash' do
  191. expect(subject).to be_a Hash
  192. end
  193. it 'contains true value' do
  194. Fabricate(:favourite, status: status, account: account)
  195. expect(subject[status.id]).to be true
  196. end
  197. end
  198. describe '.reblogs_map' do
  199. let(:status) { Fabricate(:status) }
  200. let(:account) { Fabricate(:account) }
  201. subject { Status.reblogs_map([status], account) }
  202. it 'returns a hash' do
  203. expect(subject).to be_a Hash
  204. end
  205. it 'contains true value' do
  206. Fabricate(:status, account: account, reblog: status)
  207. expect(subject[status.id]).to be true
  208. end
  209. end
  210. describe '.local_only' do
  211. it 'returns only statuses from local accounts' do
  212. local_account = Fabricate(:account, domain: nil)
  213. remote_account = Fabricate(:account, domain: 'test.com')
  214. local_status = Fabricate(:status, account: local_account)
  215. remote_status = Fabricate(:status, account: remote_account)
  216. results = described_class.local_only
  217. expect(results).to include(local_status)
  218. expect(results).not_to include(remote_status)
  219. end
  220. end
  221. describe '.as_home_timeline' do
  222. before do
  223. account = Fabricate(:account)
  224. followed = Fabricate(:account)
  225. not_followed = Fabricate(:account)
  226. Fabricate(:follow, account: account, target_account: followed)
  227. @self_status = Fabricate(:status, account: account)
  228. @followed_status = Fabricate(:status, account: followed)
  229. @not_followed_status = Fabricate(:status, account: not_followed)
  230. @results = Status.as_home_timeline(account)
  231. end
  232. it 'includes statuses from self' do
  233. expect(@results).to include(@self_status)
  234. end
  235. it 'includes statuses from followed' do
  236. expect(@results).to include(@followed_status)
  237. end
  238. it 'does not include statuses from non-followed' do
  239. expect(@results).not_to include(@not_followed_status)
  240. end
  241. end
  242. describe '.as_public_timeline' do
  243. it 'only includes statuses with public visibility' do
  244. public_status = Fabricate(:status, visibility: :public)
  245. private_status = Fabricate(:status, visibility: :private)
  246. results = Status.as_public_timeline
  247. expect(results).to include(public_status)
  248. expect(results).not_to include(private_status)
  249. end
  250. it 'does not include replies' do
  251. status = Fabricate(:status)
  252. reply = Fabricate(:status, in_reply_to_id: status.id)
  253. results = Status.as_public_timeline
  254. expect(results).to include(status)
  255. expect(results).not_to include(reply)
  256. end
  257. it 'does not include boosts' do
  258. status = Fabricate(:status)
  259. boost = Fabricate(:status, reblog_of_id: status.id)
  260. results = Status.as_public_timeline
  261. expect(results).to include(status)
  262. expect(results).not_to include(boost)
  263. end
  264. it 'filters out silenced accounts' do
  265. account = Fabricate(:account)
  266. silenced_account = Fabricate(:account, silenced: true)
  267. status = Fabricate(:status, account: account)
  268. silenced_status = Fabricate(:status, account: silenced_account)
  269. results = Status.as_public_timeline
  270. expect(results).to include(status)
  271. expect(results).not_to include(silenced_status)
  272. end
  273. context 'without local_only option' do
  274. let(:viewer) { nil }
  275. let!(:local_account) { Fabricate(:account, domain: nil) }
  276. let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
  277. let!(:local_status) { Fabricate(:status, account: local_account) }
  278. let!(:remote_status) { Fabricate(:status, account: remote_account) }
  279. subject { Status.as_public_timeline(viewer, false) }
  280. context 'without a viewer' do
  281. let(:viewer) { nil }
  282. it 'includes remote instances statuses' do
  283. expect(subject).to include(remote_status)
  284. end
  285. it 'includes local statuses' do
  286. expect(subject).to include(local_status)
  287. end
  288. end
  289. context 'with a viewer' do
  290. let(:viewer) { Fabricate(:account, username: 'viewer') }
  291. it 'includes remote instances statuses' do
  292. expect(subject).to include(remote_status)
  293. end
  294. it 'includes local statuses' do
  295. expect(subject).to include(local_status)
  296. end
  297. end
  298. end
  299. context 'with a local_only option set' do
  300. let!(:local_account) { Fabricate(:account, domain: nil) }
  301. let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
  302. let!(:local_status) { Fabricate(:status, account: local_account) }
  303. let!(:remote_status) { Fabricate(:status, account: remote_account) }
  304. subject { Status.as_public_timeline(viewer, true) }
  305. context 'without a viewer' do
  306. let(:viewer) { nil }
  307. it 'does not include remote instances statuses' do
  308. expect(subject).to include(local_status)
  309. expect(subject).not_to include(remote_status)
  310. end
  311. end
  312. context 'with a viewer' do
  313. let(:viewer) { Fabricate(:account, username: 'viewer') }
  314. it 'does not include remote instances statuses' do
  315. expect(subject).to include(local_status)
  316. expect(subject).not_to include(remote_status)
  317. end
  318. it 'is not affected by personal domain blocks' do
  319. viewer.block_domain!('test.com')
  320. expect(subject).to include(local_status)
  321. expect(subject).not_to include(remote_status)
  322. end
  323. end
  324. end
  325. describe 'with an account passed in' do
  326. before do
  327. @account = Fabricate(:account)
  328. end
  329. it 'excludes statuses from accounts blocked by the account' do
  330. blocked = Fabricate(:account)
  331. Fabricate(:block, account: @account, target_account: blocked)
  332. blocked_status = Fabricate(:status, account: blocked)
  333. results = Status.as_public_timeline(@account)
  334. expect(results).not_to include(blocked_status)
  335. end
  336. it 'excludes statuses from accounts who have blocked the account' do
  337. blocked = Fabricate(:account)
  338. Fabricate(:block, account: blocked, target_account: @account)
  339. blocked_status = Fabricate(:status, account: blocked)
  340. results = Status.as_public_timeline(@account)
  341. expect(results).not_to include(blocked_status)
  342. end
  343. it 'excludes statuses from accounts muted by the account' do
  344. muted = Fabricate(:account)
  345. Fabricate(:mute, account: @account, target_account: muted)
  346. muted_status = Fabricate(:status, account: muted)
  347. results = Status.as_public_timeline(@account)
  348. expect(results).not_to include(muted_status)
  349. end
  350. it 'excludes statuses from accounts from personally blocked domains' do
  351. blocked = Fabricate(:account, domain: 'example.com')
  352. @account.block_domain!(blocked.domain)
  353. blocked_status = Fabricate(:status, account: blocked)
  354. results = Status.as_public_timeline(@account)
  355. expect(results).not_to include(blocked_status)
  356. end
  357. context 'with language preferences' do
  358. it 'excludes statuses in languages not allowed by the account user' do
  359. user = Fabricate(:user, filtered_languages: [:fr])
  360. @account.update(user: user)
  361. en_status = Fabricate(:status, language: 'en')
  362. es_status = Fabricate(:status, language: 'es')
  363. fr_status = Fabricate(:status, language: 'fr')
  364. results = Status.as_public_timeline(@account)
  365. expect(results).to include(en_status)
  366. expect(results).to include(es_status)
  367. expect(results).not_to include(fr_status)
  368. end
  369. it 'includes all languages when user does not have a setting' do
  370. user = Fabricate(:user, filtered_languages: [])
  371. @account.update(user: user)
  372. en_status = Fabricate(:status, language: 'en')
  373. es_status = Fabricate(:status, language: 'es')
  374. results = Status.as_public_timeline(@account)
  375. expect(results).to include(en_status)
  376. expect(results).to include(es_status)
  377. end
  378. it 'includes all languages when account does not have a user' do
  379. expect(@account.user).to be_nil
  380. en_status = Fabricate(:status, language: 'en')
  381. es_status = Fabricate(:status, language: 'es')
  382. results = Status.as_public_timeline(@account)
  383. expect(results).to include(en_status)
  384. expect(results).to include(es_status)
  385. end
  386. end
  387. context 'where that account is silenced' do
  388. it 'includes statuses from other accounts that are silenced' do
  389. @account.update(silenced: true)
  390. other_silenced_account = Fabricate(:account, silenced: true)
  391. other_status = Fabricate(:status, account: other_silenced_account)
  392. results = Status.as_public_timeline(@account)
  393. expect(results).to include(other_status)
  394. end
  395. end
  396. end
  397. end
  398. describe '.as_tag_timeline' do
  399. it 'includes statuses with a tag' do
  400. tag = Fabricate(:tag)
  401. status = Fabricate(:status, tags: [tag])
  402. other = Fabricate(:status)
  403. results = Status.as_tag_timeline(tag)
  404. expect(results).to include(status)
  405. expect(results).not_to include(other)
  406. end
  407. it 'allows replies to be included' do
  408. original = Fabricate(:status)
  409. tag = Fabricate(:tag)
  410. status = Fabricate(:status, tags: [tag], in_reply_to_id: original.id)
  411. results = Status.as_tag_timeline(tag)
  412. expect(results).to include(status)
  413. end
  414. end
  415. describe '.permitted_for' do
  416. subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
  417. let(:target_account) { alice }
  418. let(:account) { bob }
  419. let!(:public_status) { Fabricate(:status, account: target_account, visibility: 'public') }
  420. let!(:unlisted_status) { Fabricate(:status, account: target_account, visibility: 'unlisted') }
  421. let!(:private_status) { Fabricate(:status, account: target_account, visibility: 'private') }
  422. let!(:direct_status) do
  423. Fabricate(:status, account: target_account, visibility: 'direct').tap do |status|
  424. Fabricate(:mention, status: status, account: account)
  425. end
  426. end
  427. let!(:other_direct_status) do
  428. Fabricate(:status, account: target_account, visibility: 'direct').tap do |status|
  429. Fabricate(:mention, status: status)
  430. end
  431. end
  432. context 'given nil' do
  433. let(:account) { nil }
  434. let(:direct_status) { nil }
  435. it { is_expected.to eq(%w(unlisted public)) }
  436. end
  437. context 'given blocked account' do
  438. before do
  439. target_account.block!(account)
  440. end
  441. it { is_expected.to be_empty }
  442. end
  443. context 'given same account' do
  444. let(:account) { target_account }
  445. it { is_expected.to eq(%w(direct direct private unlisted public)) }
  446. end
  447. context 'given followed account' do
  448. before do
  449. account.follow!(target_account)
  450. end
  451. it { is_expected.to eq(%w(direct private unlisted public)) }
  452. end
  453. context 'given unfollowed account' do
  454. it { is_expected.to eq(%w(direct unlisted public)) }
  455. end
  456. end
  457. describe 'before_create' do
  458. it 'sets account being replied to correctly over intermediary nodes' do
  459. first_status = Fabricate(:status, account: bob)
  460. intermediary = Fabricate(:status, thread: first_status, account: alice)
  461. final = Fabricate(:status, thread: intermediary, account: alice)
  462. expect(final.in_reply_to_account_id).to eq bob.id
  463. end
  464. it 'creates new conversation for stand-alone status' do
  465. expect(Status.create(account: alice, text: 'First').conversation_id).to_not be_nil
  466. end
  467. it 'keeps conversation of parent node' do
  468. parent = Fabricate(:status, text: 'First')
  469. expect(Status.create(account: alice, thread: parent, text: 'Response').conversation_id).to eq parent.conversation_id
  470. end
  471. end
  472. end