replies_controller_spec.rb 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. # frozen_string_literal: true
  2. require 'rails_helper'
  3. RSpec.describe ActivityPub::RepliesController do
  4. let(:status) { Fabricate(:status, visibility: parent_visibility) }
  5. let(:remote_account) { Fabricate(:account, domain: 'foobar.com') }
  6. let(:remote_reply_id) { 'https://foobar.com/statuses/1234' }
  7. let(:remote_querier) { nil }
  8. shared_examples 'common behavior' do
  9. context 'when status is private' do
  10. let(:parent_visibility) { :private }
  11. it 'returns http not found' do
  12. expect(response).to have_http_status(404)
  13. end
  14. end
  15. context 'when status is direct' do
  16. let(:parent_visibility) { :direct }
  17. it 'returns http not found' do
  18. expect(response).to have_http_status(404)
  19. end
  20. end
  21. end
  22. shared_examples 'disallowed access' do
  23. context 'when status is public' do
  24. let(:parent_visibility) { :public }
  25. it 'returns http not found' do
  26. expect(response).to have_http_status(404)
  27. end
  28. end
  29. it_behaves_like 'common behavior'
  30. end
  31. shared_examples 'allowed access' do
  32. context 'when account is permanently suspended' do
  33. let(:parent_visibility) { :public }
  34. before do
  35. status.account.suspend!
  36. status.account.deletion_request.destroy
  37. end
  38. it 'returns http gone' do
  39. expect(response).to have_http_status(410)
  40. end
  41. end
  42. context 'when account is temporarily suspended' do
  43. let(:parent_visibility) { :public }
  44. before do
  45. status.account.suspend!
  46. end
  47. it 'returns http forbidden' do
  48. expect(response).to have_http_status(403)
  49. end
  50. end
  51. context 'when status is public' do
  52. let(:parent_visibility) { :public }
  53. it 'returns http success and correct media type' do
  54. expect(response)
  55. .to have_http_status(200)
  56. .and have_cacheable_headers
  57. expect(response.media_type).to eq 'application/activity+json'
  58. end
  59. context 'without only_other_accounts' do
  60. it "returns items with thread author's replies" do
  61. expect(response.parsed_body)
  62. .to include(
  63. first: be_a(Hash).and(
  64. include(
  65. items: be_an(Array)
  66. .and(have_attributes(size: 1))
  67. .and(all(satisfy { |item| targets_public_collection?(item) }))
  68. )
  69. )
  70. )
  71. end
  72. context 'when there are few self-replies' do
  73. it 'points next to replies from other people' do
  74. expect(response.parsed_body)
  75. .to include(
  76. first: be_a(Hash).and(
  77. include(
  78. next: satisfy { |value| (parsed_uri_query_values(value) & %w(only_other_accounts=true page=true)).any? }
  79. )
  80. )
  81. )
  82. end
  83. end
  84. context 'when there are many self-replies' do
  85. before do
  86. 10.times { Fabricate(:status, account: status.account, thread: status, visibility: :public) }
  87. end
  88. it 'points next to other self-replies' do
  89. expect(response.parsed_body)
  90. .to include(
  91. first: be_a(Hash).and(
  92. include(
  93. next: satisfy { |value| (parsed_uri_query_values(value) & %w(only_other_accounts=false page=true)).any? }
  94. )
  95. )
  96. )
  97. end
  98. end
  99. end
  100. context 'with only_other_accounts' do
  101. let(:only_other_accounts) { 'true' }
  102. it 'returns items with other public or unlisted replies' do
  103. expect(response.parsed_body)
  104. .to include(
  105. first: be_a(Hash).and(
  106. include(items: be_an(Array).and(have_attributes(size: 3)))
  107. )
  108. )
  109. end
  110. it 'only inlines items that are local and public or unlisted replies' do
  111. expect(inlined_replies)
  112. .to all(satisfy { |item| targets_public_collection?(item) })
  113. .and all(satisfy { |item| ActivityPub::TagManager.instance.local_uri?(item[:id]) })
  114. end
  115. it 'uses ids for remote toots' do
  116. expect(remote_replies)
  117. .to all(satisfy { |item| item.is_a?(String) && !ActivityPub::TagManager.instance.local_uri?(item) })
  118. end
  119. context 'when there are few replies' do
  120. it 'does not have a next page' do
  121. expect(response.parsed_body)
  122. .to include(
  123. first: be_a(Hash).and(not_include(next: be_present))
  124. )
  125. end
  126. end
  127. context 'when there are many replies' do
  128. before do
  129. 10.times { Fabricate(:status, thread: status, visibility: :public) }
  130. end
  131. it 'points next to other replies' do
  132. expect(response.parsed_body)
  133. .to include(
  134. first: be_a(Hash).and(
  135. include(
  136. next: satisfy { |value| (parsed_uri_query_values(value) & %w(only_other_accounts=true page=true)).any? }
  137. )
  138. )
  139. )
  140. end
  141. end
  142. end
  143. end
  144. it_behaves_like 'common behavior'
  145. end
  146. before do
  147. stub_const 'ActivityPub::RepliesController::DESCENDANTS_LIMIT', 5
  148. allow(controller).to receive(:signed_request_actor).and_return(remote_querier)
  149. Fabricate(:status, thread: status, visibility: :public)
  150. Fabricate(:status, thread: status, visibility: :public)
  151. Fabricate(:status, thread: status, visibility: :private)
  152. Fabricate(:status, account: status.account, thread: status, visibility: :public)
  153. Fabricate(:status, account: status.account, thread: status, visibility: :private)
  154. Fabricate(:status, account: remote_account, thread: status, visibility: :public, uri: remote_reply_id)
  155. end
  156. describe 'GET #index' do
  157. subject(:response) { get :index, params: { account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts } }
  158. let(:only_other_accounts) { nil }
  159. context 'with no signature' do
  160. it_behaves_like 'allowed access'
  161. end
  162. context 'with signature' do
  163. let(:remote_querier) { Fabricate(:account, domain: 'example.com') }
  164. it_behaves_like 'allowed access'
  165. context 'when signed request account is blocked' do
  166. before do
  167. status.account.block!(remote_querier)
  168. end
  169. it_behaves_like 'disallowed access'
  170. end
  171. context 'when signed request account is domain blocked' do
  172. before do
  173. status.account.block_domain!(remote_querier.domain)
  174. end
  175. it_behaves_like 'disallowed access'
  176. end
  177. end
  178. end
  179. private
  180. def inlined_replies
  181. response
  182. .parsed_body[:first][:items]
  183. .select { |x| x.is_a?(Hash) }
  184. end
  185. def remote_replies
  186. response
  187. .parsed_body[:first][:items]
  188. .reject { |x| x.is_a?(Hash) }
  189. end
  190. def parsed_uri_query_values(uri)
  191. Addressable::URI
  192. .parse(uri)
  193. .query
  194. .split('&')
  195. end
  196. def ap_public_collection
  197. ActivityPub::TagManager::COLLECTIONS[:public]
  198. end
  199. def targets_public_collection?(item)
  200. item[:to].include?(ap_public_collection) || item[:cc].include?(ap_public_collection)
  201. end
  202. end