Allow viewing and severing relationships with suspended accounts (#27667)
This commit is contained in:
		
					parent
					
						
							
								b87bfb8c96
							
						
					
				
			
			
				commit
				
					
						c451bbe249
					
				
			
		
					 7 changed files with 157 additions and 121 deletions
				
			
		|  | @ -5,10 +5,11 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController | ||||||
|   before_action :require_user! |   before_action :require_user! | ||||||
| 
 | 
 | ||||||
|   def index |   def index | ||||||
|     accounts = Account.without_suspended.where(id: account_ids).select('id') |     scope = Account.where(id: account_ids).select('id') | ||||||
|  |     scope.merge!(Account.without_suspended) unless truthy_param?(:with_suspended) | ||||||
|     # .where doesn't guarantee that our results are in the same order |     # .where doesn't guarantee that our results are in the same order | ||||||
|     # we requested them, so return the "right" order to the requestor. |     # we requested them, so return the "right" order to the requestor. | ||||||
|     @accounts = accounts.index_by(&:id).values_at(*account_ids).compact |     @accounts = scope.index_by(&:id).values_at(*account_ids).compact | ||||||
|     render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships |     render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -460,7 +460,7 @@ export function fetchRelationships(accountIds) { | ||||||
| 
 | 
 | ||||||
|     dispatch(fetchRelationshipsRequest(newAccountIds)); |     dispatch(fetchRelationshipsRequest(newAccountIds)); | ||||||
| 
 | 
 | ||||||
|     api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { |     api(getState).get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { | ||||||
|       dispatch(fetchRelationshipsSuccess({ relationships: response.data })); |       dispatch(fetchRelationshipsSuccess({ relationships: response.data })); | ||||||
|     }).catch(error => { |     }).catch(error => { | ||||||
|       dispatch(fetchRelationshipsFail(error)); |       dispatch(fetchRelationshipsFail(error)); | ||||||
|  |  | ||||||
|  | @ -119,7 +119,7 @@ class Account extends ImmutablePureComponent { | ||||||
|         buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />; |         buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />; | ||||||
|       } else if (defaultAction === 'block') { |       } else if (defaultAction === 'block') { | ||||||
|         buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />; |         buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />; | ||||||
|       } else if (!account.get('moved') || following) { |       } else if (!account.get('suspended') && !account.get('moved') || following) { | ||||||
|         buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />; |         buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -289,7 +289,7 @@ class Header extends ImmutablePureComponent { | ||||||
|       lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />; |       lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (signedIn && account.get('id') !== me) { |     if (signedIn && account.get('id') !== me && !account.get('suspended')) { | ||||||
|       menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); |       menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); | ||||||
|       menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); |       menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); | ||||||
|       menu.push(null); |       menu.push(null); | ||||||
|  | @ -299,7 +299,7 @@ class Header extends ImmutablePureComponent { | ||||||
|       menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') }); |       menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if ('share' in navigator) { |     if ('share' in navigator && !account.get('suspended')) { | ||||||
|       menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); |       menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); | ||||||
|       menu.push(null); |       menu.push(null); | ||||||
|     } |     } | ||||||
|  | @ -347,7 +347,9 @@ class Header extends ImmutablePureComponent { | ||||||
|         menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true }); |         menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true }); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true }); |       if (!account.get('suspended')) { | ||||||
|  |         menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true }); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (signedIn && isRemote) { |     if (signedIn && isRemote) { | ||||||
|  | @ -395,7 +397,7 @@ class Header extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|         <div className='account__header__image'> |         <div className='account__header__image'> | ||||||
|           <div className='account__header__info'> |           <div className='account__header__info'> | ||||||
|             {!suspended && info} |             {info} | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|           {!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />} |           {!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />} | ||||||
|  | @ -407,18 +409,16 @@ class Header extends ImmutablePureComponent { | ||||||
|               <Avatar account={suspended || hidden ? undefined : account} size={90} /> |               <Avatar account={suspended || hidden ? undefined : account} size={90} /> | ||||||
|             </a> |             </a> | ||||||
| 
 | 
 | ||||||
|             {!suspended && ( |             <div className='account__header__tabs__buttons'> | ||||||
|               <div className='account__header__tabs__buttons'> |               {!hidden && ( | ||||||
|                 {!hidden && ( |                 <> | ||||||
|                   <> |                   {actionBtn} | ||||||
|                     {actionBtn} |                   {bellBtn} | ||||||
|                     {bellBtn} |                 </> | ||||||
|                   </> |               )} | ||||||
|                 )} |  | ||||||
| 
 | 
 | ||||||
|                 <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' /> |               <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' /> | ||||||
|               </div> |             </div> | ||||||
|             )} |  | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|           <div className='account__header__tabs__name'> |           <div className='account__header__tabs__name'> | ||||||
|  |  | ||||||
|  | @ -301,6 +301,10 @@ namespace :api, format: false do | ||||||
|       resources :statuses, only: [:show, :destroy] |       resources :statuses, only: [:show, :destroy] | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     namespace :accounts do | ||||||
|  |       resources :relationships, only: :index | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     namespace :admin do |     namespace :admin do | ||||||
|       resources :accounts, only: [:index] |       resources :accounts, only: [:index] | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -1,102 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| require 'rails_helper' |  | ||||||
| 
 |  | ||||||
| describe Api::V1::Accounts::RelationshipsController do |  | ||||||
|   render_views |  | ||||||
| 
 |  | ||||||
|   let(:user)  { Fabricate(:user) } |  | ||||||
|   let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') } |  | ||||||
| 
 |  | ||||||
|   before do |  | ||||||
|     allow(controller).to receive(:doorkeeper_token) { token } |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   describe 'GET #index' do |  | ||||||
|     let(:simon) { Fabricate(:account) } |  | ||||||
|     let(:lewis) { Fabricate(:account) } |  | ||||||
| 
 |  | ||||||
|     before do |  | ||||||
|       user.account.follow!(simon) |  | ||||||
|       lewis.follow!(user.account) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when provided only one ID' do |  | ||||||
|       before do |  | ||||||
|         get :index, params: { id: simon.id } |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns JSON with correct data', :aggregate_failures do |  | ||||||
|         json = body_as_json |  | ||||||
| 
 |  | ||||||
|         expect(response).to have_http_status(200) |  | ||||||
|         expect(json).to be_a Enumerable |  | ||||||
|         expect(json.first[:following]).to be true |  | ||||||
|         expect(json.first[:followed_by]).to be false |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when provided multiple IDs' do |  | ||||||
|       before do |  | ||||||
|         get :index, params: { id: [simon.id, lewis.id] } |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns http success' do |  | ||||||
|         expect(response).to have_http_status(200) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       context 'when there is returned JSON data' do |  | ||||||
|         let(:json) { body_as_json } |  | ||||||
| 
 |  | ||||||
|         it 'returns an enumerable json with correct elements', :aggregate_failures do |  | ||||||
|           expect(json).to be_a Enumerable |  | ||||||
| 
 |  | ||||||
|           expect_simon_item_one |  | ||||||
|           expect_lewis_item_two |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         def expect_simon_item_one |  | ||||||
|           expect(json.first[:id]).to eq simon.id.to_s |  | ||||||
|           expect(json.first[:following]).to be true |  | ||||||
|           expect(json.first[:showing_reblogs]).to be true |  | ||||||
|           expect(json.first[:followed_by]).to be false |  | ||||||
|           expect(json.first[:muting]).to be false |  | ||||||
|           expect(json.first[:requested]).to be false |  | ||||||
|           expect(json.first[:domain_blocking]).to be false |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         def expect_lewis_item_two |  | ||||||
|           expect(json.second[:id]).to eq lewis.id.to_s |  | ||||||
|           expect(json.second[:following]).to be false |  | ||||||
|           expect(json.second[:showing_reblogs]).to be false |  | ||||||
|           expect(json.second[:followed_by]).to be true |  | ||||||
|           expect(json.second[:muting]).to be false |  | ||||||
|           expect(json.second[:requested]).to be false |  | ||||||
|           expect(json.second[:domain_blocking]).to be false |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns JSON with correct data on cached requests too' do |  | ||||||
|         get :index, params: { id: [simon.id] } |  | ||||||
| 
 |  | ||||||
|         json = body_as_json |  | ||||||
| 
 |  | ||||||
|         expect(json).to be_a Enumerable |  | ||||||
|         expect(json.first[:following]).to be true |  | ||||||
|         expect(json.first[:showing_reblogs]).to be true |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns JSON with correct data after change too' do |  | ||||||
|         user.account.unfollow!(simon) |  | ||||||
| 
 |  | ||||||
|         get :index, params: { id: [simon.id] } |  | ||||||
| 
 |  | ||||||
|         json = body_as_json |  | ||||||
| 
 |  | ||||||
|         expect(json).to be_a Enumerable |  | ||||||
|         expect(json.first[:following]).to be false |  | ||||||
|         expect(json.first[:showing_reblogs]).to be false |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
							
								
								
									
										133
									
								
								spec/requests/api/v1/accounts/relationships_spec.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								spec/requests/api/v1/accounts/relationships_spec.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,133 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | describe 'GET /api/v1/accounts/relationships' do | ||||||
|  |   subject do | ||||||
|  |     get '/api/v1/accounts/relationships', headers: headers, params: params | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   let(:user)    { Fabricate(:user) } | ||||||
|  |   let(:scopes)  { 'read:follows' } | ||||||
|  |   let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } | ||||||
|  |   let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } | ||||||
|  | 
 | ||||||
|  |   let(:simon) { Fabricate(:account) } | ||||||
|  |   let(:lewis) { Fabricate(:account) } | ||||||
|  |   let(:bob)   { Fabricate(:account, suspended: true) } | ||||||
|  | 
 | ||||||
|  |   before do | ||||||
|  |     user.account.follow!(simon) | ||||||
|  |     lewis.follow!(user.account) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   context 'when provided only one ID' do | ||||||
|  |     let(:params) { { id: simon.id } } | ||||||
|  | 
 | ||||||
|  |     it 'returns JSON with correct data', :aggregate_failures do | ||||||
|  |       subject | ||||||
|  | 
 | ||||||
|  |       json = body_as_json | ||||||
|  | 
 | ||||||
|  |       expect(response).to have_http_status(200) | ||||||
|  |       expect(json).to be_a Enumerable | ||||||
|  |       expect(json.first[:following]).to be true | ||||||
|  |       expect(json.first[:followed_by]).to be false | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   context 'when provided multiple IDs' do | ||||||
|  |     let(:params) { { id: [simon.id, lewis.id, bob.id] } } | ||||||
|  | 
 | ||||||
|  |     context 'when there is returned JSON data' do | ||||||
|  |       let(:json) { body_as_json } | ||||||
|  | 
 | ||||||
|  |       context 'with default parameters' do | ||||||
|  |         it 'returns an enumerable json with correct elements, excluding suspended accounts', :aggregate_failures do | ||||||
|  |           subject | ||||||
|  | 
 | ||||||
|  |           expect(response).to have_http_status(200) | ||||||
|  |           expect(json).to be_a Enumerable | ||||||
|  |           expect(json.size).to eq 2 | ||||||
|  | 
 | ||||||
|  |           expect_simon_item_one | ||||||
|  |           expect_lewis_item_two | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'with `with_suspended` parameter' do | ||||||
|  |         let(:params) { { id: [simon.id, lewis.id, bob.id], with_suspended: true } } | ||||||
|  | 
 | ||||||
|  |         it 'returns an enumerable json with correct elements, including suspended accounts', :aggregate_failures do | ||||||
|  |           subject | ||||||
|  | 
 | ||||||
|  |           expect(response).to have_http_status(200) | ||||||
|  |           expect(json).to be_a Enumerable | ||||||
|  |           expect(json.size).to eq 3 | ||||||
|  | 
 | ||||||
|  |           expect_simon_item_one | ||||||
|  |           expect_lewis_item_two | ||||||
|  |           expect_bob_item_three | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def expect_simon_item_one | ||||||
|  |         expect(json.first[:id]).to eq simon.id.to_s | ||||||
|  |         expect(json.first[:following]).to be true | ||||||
|  |         expect(json.first[:showing_reblogs]).to be true | ||||||
|  |         expect(json.first[:followed_by]).to be false | ||||||
|  |         expect(json.first[:muting]).to be false | ||||||
|  |         expect(json.first[:requested]).to be false | ||||||
|  |         expect(json.first[:domain_blocking]).to be false | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def expect_lewis_item_two | ||||||
|  |         expect(json.second[:id]).to eq lewis.id.to_s | ||||||
|  |         expect(json.second[:following]).to be false | ||||||
|  |         expect(json.second[:showing_reblogs]).to be false | ||||||
|  |         expect(json.second[:followed_by]).to be true | ||||||
|  |         expect(json.second[:muting]).to be false | ||||||
|  |         expect(json.second[:requested]).to be false | ||||||
|  |         expect(json.second[:domain_blocking]).to be false | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def expect_bob_item_three | ||||||
|  |         expect(json.third[:id]).to eq bob.id.to_s | ||||||
|  |         expect(json.third[:following]).to be false | ||||||
|  |         expect(json.third[:showing_reblogs]).to be false | ||||||
|  |         expect(json.third[:followed_by]).to be false | ||||||
|  |         expect(json.third[:muting]).to be false | ||||||
|  |         expect(json.third[:requested]).to be false | ||||||
|  |         expect(json.third[:domain_blocking]).to be false | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns JSON with correct data on cached requests too' do | ||||||
|  |       subject | ||||||
|  |       subject | ||||||
|  | 
 | ||||||
|  |       expect(response).to have_http_status(200) | ||||||
|  | 
 | ||||||
|  |       json = body_as_json | ||||||
|  | 
 | ||||||
|  |       expect(json).to be_a Enumerable | ||||||
|  |       expect(json.first[:following]).to be true | ||||||
|  |       expect(json.first[:showing_reblogs]).to be true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns JSON with correct data after change too' do | ||||||
|  |       subject | ||||||
|  |       user.account.unfollow!(simon) | ||||||
|  | 
 | ||||||
|  |       get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id] } | ||||||
|  | 
 | ||||||
|  |       expect(response).to have_http_status(200) | ||||||
|  | 
 | ||||||
|  |       json = body_as_json | ||||||
|  | 
 | ||||||
|  |       expect(json).to be_a Enumerable | ||||||
|  |       expect(json.first[:following]).to be false | ||||||
|  |       expect(json.first[:showing_reblogs]).to be false | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue