From 5cff7910c2c519af2d255454b66b0bfa6cf5288c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 3 May 2020 22:19:24 +0200 Subject: Add more ActivityPub controller tests (#13590) --- spec/controllers/accounts_controller_spec.rb | 632 ++++++++++++++++++++++++--- 1 file changed, 566 insertions(+), 66 deletions(-) (limited to 'spec/controllers') diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 3d2a0665d..bd36f5494 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -3,108 +3,608 @@ require 'rails_helper' RSpec.describe AccountsController, type: :controller do render_views - let(:alice) { Fabricate(:account, username: 'alice', user: Fabricate(:user)) } - let(:eve) { Fabricate(:user) } + let(:account) { Fabricate(:user).account } describe 'GET #show' do - let!(:status1) { Status.create!(account: alice, text: 'Hello world') } - let!(:status2) { Status.create!(account: alice, text: 'Boop', thread: status1) } - let!(:status3) { Status.create!(account: alice, text: 'Picture!') } - let!(:status4) { Status.create!(account: alice, text: 'Mentioning @alice') } - let!(:status5) { Status.create!(account: alice, text: 'Kitsune') } - let!(:status6) { Status.create!(account: alice, text: 'Neko') } - let!(:status7) { Status.create!(account: alice, text: 'Tanuki') } - - let!(:status_pin1) { StatusPin.create!(account: alice, status: status5, created_at: 5.days.ago) } - let!(:status_pin2) { StatusPin.create!(account: alice, status: status6, created_at: 2.years.ago) } - let!(:status_pin3) { StatusPin.create!(account: alice, status: status7, created_at: 10.minutes.ago) } + let(:format) { 'html' } + + let!(:status) { Fabricate(:status, account: account) } + let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) } + let!(:status_self_reply) { Fabricate(:status, account: account, thread: status) } + let!(:status_media) { Fabricate(:status, account: account) } + let!(:status_pinned) { Fabricate(:status, account: account) } + let!(:status_private) { Fabricate(:status, account: account, visibility: :private) } + let!(:status_direct) { Fabricate(:status, account: account, visibility: :direct) } + let!(:status_reblog) { Fabricate(:status, account: account, reblog: Fabricate(:status)) } before do - alice.block!(eve.account) - status3.media_attachments.create!(account: alice, file: fixture_file_upload('files/attachment.jpg', 'image/jpeg')) + status_media.media_attachments << Fabricate(:media_attachment, account: account, type: :image) + account.pinned_statuses << status_pinned end - shared_examples 'responses' do - before do - sign_in(current_user) if defined? current_user - get :show, params: { - username: alice.username, - max_id: (max_id if defined? max_id), - since_id: (since_id if defined? since_id), - current_user: (current_user if defined? current_user), - }, format: format + shared_examples 'preliminary checks' do + context 'when account is not approved' do + before do + account.user.update(approved: false) + end + + it 'returns http not found' do + get :show, params: { username: account.username, format: format } + expect(response).to have_http_status(404) + end + end + + context 'when account is suspended' do + before do + account.suspend! + end + + it 'returns http gone' do + get :show, params: { username: account.username, format: format } + expect(response).to have_http_status(410) + end end + end + + context 'as HTML' do + let(:format) { 'html' } + + it_behaves_like 'preliminary checks' + + shared_examples 'common response characteristics' do + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account) + end - it 'assigns @account' do - expect(assigns(:account)).to eq alice + it 'renders show template' do + expect(response).to render_template(:show) + end end - it 'returns http success' do - expect(response).to have_http_status(200) + context do + before do + get :show, params: { username: account.username, format: format } + end + + it_behaves_like 'common response characteristics' + + it 'renders public status' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) + end + + it 'renders self-reply' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply)) + end + + it 'renders status with media' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) + end + + it 'renders reblog' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) + end + + it 'renders pinned status' do + expect(response.body).to include(I18n.t('stream_entries.pinned')) + end + + it 'does not render private status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) + end + + it 'does not render direct status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) + end + + it 'does not render reply to someone else' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) + end end - it 'returns correct format' do - expect(response.content_type).to eq content_type + context 'when signed-in' do + let(:user) { Fabricate(:user) } + + before do + sign_in(user) + end + + context 'when user follows account' do + before do + user.account.follow!(account) + get :show, params: { username: account.username, format: format } + end + + it 'does not render private status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) + end + end + + context 'when user is blocked' do + before do + account.block!(user.account) + get :show, params: { username: account.username, format: format } + end + + it 'renders unavailable message' do + expect(response.body).to include(I18n.t('accounts.unavailable')) + end + + it 'does not render public status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status)) + end + + it 'does not render self-reply' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply)) + end + + it 'does not render status with media' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media)) + end + + it 'does not render reblog' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) + end + + it 'does not render pinned status' do + expect(response.body).to_not include(I18n.t('stream_entries.pinned')) + end + + it 'does not render private status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) + end + + it 'does not render direct status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) + end + + it 'does not render reply to someone else' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) + end + end + end + + context 'with replies' do + before do + allow(controller).to receive(:replies_requested?).and_return(true) + get :show, params: { username: account.username, format: format } + end + + it_behaves_like 'common response characteristics' + + it 'renders public status' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) + end + + it 'renders self-reply' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply)) + end + + it 'renders status with media' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) + end + + it 'renders reblog' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) + end + + it 'does not render pinned status' do + expect(response.body).to_not include(I18n.t('stream_entries.pinned')) + end + + it 'does not render private status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) + end + + it 'does not render direct status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) + end + + it 'renders reply to someone else' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reply)) + end + end + + context 'with media' do + before do + allow(controller).to receive(:media_requested?).and_return(true) + get :show, params: { username: account.username, format: format } + end + + it_behaves_like 'common response characteristics' + + it 'does not render public status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status)) + end + + it 'does not render self-reply' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply)) + end + + it 'renders status with media' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) + end + + it 'does not render reblog' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) + end + + it 'does not render pinned status' do + expect(response.body).to_not include(I18n.t('stream_entries.pinned')) + end + + it 'does not render private status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) + end + + it 'does not render direct status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) + end + + it 'does not render reply to someone else' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) + end + end + + context 'with tag' do + let(:tag) { Fabricate(:tag) } + + let!(:status_tag) { Fabricate(:status, account: account) } + + before do + allow(controller).to receive(:tag_requested?).and_return(true) + status_tag.tags << tag + get :show, params: { username: account.username, format: format, tag: tag.to_param } + end + + it_behaves_like 'common response characteristics' + + it 'does not render public status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status)) + end + + it 'does not render self-reply' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply)) + end + + it 'does not render status with media' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media)) + end + + it 'does not render reblog' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) + end + + it 'does not render pinned status' do + expect(response.body).to_not include(I18n.t('stream_entries.pinned')) + end + + it 'does not render private status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) + end + + it 'does not render direct status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) + end + + it 'does not render reply to someone else' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) + end + + it 'renders status with tag' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_tag)) + end end end - context 'activitystreams2' do + context 'as JSON' do + let(:authorized_fetch_mode) { false } let(:format) { 'json' } - let(:content_type) { 'application/activity+json' } - include_examples 'responses' - end + before do + allow(controller).to receive(:authorized_fetch_mode?).and_return(authorized_fetch_mode) + end + + it_behaves_like 'preliminary checks' + + context do + before do + get :show, params: { username: account.username, format: format } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + it 'renders account' do + json = body_as_json + expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + end + + context 'in authorized fetch mode' do + let(:authorized_fetch_mode) { true } - context 'html' do - let(:format) { nil } - let(:content_type) { 'text/html' } + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + it 'returns Vary header with Signature' do + expect(response.headers['Vary']).to include 'Signature' + end - shared_examples 'responsed statuses' do - it 'assigns @pinned_statuses' do - pinned_statuses = assigns(:pinned_statuses).to_a - expect(pinned_statuses.size).to eq expected_pinned_statuses.size - pinned_statuses.each.zip(expected_pinned_statuses.each) do |pinned_status, expected_pinned_status| - expect(pinned_status).to eq expected_pinned_status + it 'renders bare minimum account' do + json = body_as_json + expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey) + expect(json).to_not include(:name, :summary) end end + end + + context 'when signed in' do + let(:user) { Fabricate(:user) } + + before do + sign_in(user) + get :show, params: { username: account.username, format: format } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + it 'renders account' do + json = body_as_json + expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + end + end + + context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com') } + + before do + allow(controller).to receive(:signed_request_account).and_return(remote_account) + get :show, params: { username: account.username, format: format } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + it 'renders account' do + json = body_as_json + expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + end + + context 'in authorized fetch mode' do + let(:authorized_fetch_mode) { true } - it 'assigns @statuses' do - statuses = assigns(:statuses).to_a - expect(statuses.size).to eq expected_statuses.size - statuses.each.zip(expected_statuses.each) do |status, expected_status| - expect(status).to eq expected_status + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'private' + end + + it 'returns Vary header with Signature' do + expect(response.headers['Vary']).to include 'Signature' + end + + it 'renders account' do + json = body_as_json + expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) end end end + end + + context 'as RSS' do + let(:format) { 'rss' } - include_examples 'responses' + it_behaves_like 'preliminary checks' + + shared_examples 'common response characteristics' do + it 'returns http success' do + expect(response).to have_http_status(200) + end - context 'with anonymous visitor' do - context 'without since_id nor max_id' do - let(:expected_statuses) { [status7, status6, status5, status4, status3, status2, status1] } - let(:expected_pinned_statuses) { [status7, status5, status6] } + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + end - include_examples 'responsed statuses' + context do + before do + get :show, params: { username: account.username, format: format } end - context 'with since_id nor max_id' do - let(:max_id) { status4.id } - let(:since_id) { status1.id } - let(:expected_statuses) { [status3, status2] } - let(:expected_pinned_statuses) { [] } + it_behaves_like 'common response characteristics' + + it 'renders public status' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) + end - include_examples 'responsed statuses' + it 'renders self-reply' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply)) + end + + it 'renders status with media' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) + end + + it 'does not render reblog' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) + end + + it 'does not render private status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) + end + + it 'does not render direct status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) + end + + it 'does not render reply to someone else' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) + end + end + + context 'with replies' do + before do + allow(controller).to receive(:replies_requested?).and_return(true) + get :show, params: { username: account.username, format: format } + end + + it_behaves_like 'common response characteristics' + + it 'renders public status' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) + end + + it 'renders self-reply' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply)) + end + + it 'renders status with media' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) + end + + it 'does not render reblog' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) + end + + it 'does not render private status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) + end + + it 'does not render direct status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) + end + + it 'renders reply to someone else' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reply)) end end - context 'with blocked visitor' do - let(:current_user) { eve } + context 'with media' do + before do + allow(controller).to receive(:media_requested?).and_return(true) + get :show, params: { username: account.username, format: format } + end + + it_behaves_like 'common response characteristics' + + it 'does not render public status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status)) + end - context 'without since_id nor max_id' do - let(:expected_statuses) { [] } - let(:expected_pinned_statuses) { [] } + it 'does not render self-reply' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply)) + end + + it 'renders status with media' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) + end + + it 'does not render reblog' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) + end + + it 'does not render private status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) + end + + it 'does not render direct status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) + end + + it 'does not render reply to someone else' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) + end + end + + context 'with tag' do + let(:tag) { Fabricate(:tag) } + + let!(:status_tag) { Fabricate(:status, account: account) } + + before do + allow(controller).to receive(:tag_requested?).and_return(true) + status_tag.tags << tag + get :show, params: { username: account.username, format: format, tag: tag.to_param } + end + + it_behaves_like 'common response characteristics' + + it 'does not render public status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status)) + end + + it 'does not render self-reply' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply)) + end + + it 'does not render status with media' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media)) + end + + it 'does not render reblog' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) + end + + it 'does not render private status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) + end + + it 'does not render direct status' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) + end + + it 'does not render reply to someone else' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) + end - include_examples 'responsed statuses' + it 'renders status with tag' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_tag)) end end end -- cgit From f1e0fa80f67365e443ed56fc9e907b3ddf5f1524 Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 8 May 2020 20:36:34 +0200 Subject: Fix own following/followers not showing muted users (#13614) Fixes #13612 --- .../v1/accounts/follower_accounts_controller.rb | 2 +- .../v1/accounts/following_accounts_controller.rb | 2 +- .../accounts/follower_accounts_controller_spec.rb | 23 ++++++++++++++++++++++ .../accounts/following_accounts_controller_spec.rb | 23 ++++++++++++++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 1daa1ed0d..2277067c9 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -20,7 +20,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController return [] if hide_results? scope = default_accounts - scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? + scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id scope.merge(paginated_follows).to_a end diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 6fc23cf75..93d4bd3a4 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -20,7 +20,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController return [] if hide_results? scope = default_accounts - scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? + scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id scope.merge(paginated_follows).to_a end diff --git a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb index 54587187f..482a19ef2 100644 --- a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb @@ -36,5 +36,28 @@ describe Api::V1::Accounts::FollowerAccountsController do expect(body_as_json.size).to eq 1 expect(body_as_json[0][:id]).to eq alice.id.to_s end + + context 'when requesting user is blocked' do + before do + account.block!(user.account) + end + + it 'hides results' do + get :index, params: { account_id: account.id, limit: 2 } + expect(body_as_json.size).to eq 0 + end + end + + context 'when requesting user is the account owner' do + let(:user) { Fabricate(:user, account: account) } + + it 'returns all accounts, including muted accounts' do + user.account.mute!(bob) + get :index, params: { account_id: account.id, limit: 2 } + + expect(body_as_json.size).to eq 2 + expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s]) + end + end end end diff --git a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb index a580a7368..e35b625fe 100644 --- a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb @@ -36,5 +36,28 @@ describe Api::V1::Accounts::FollowingAccountsController do expect(body_as_json.size).to eq 1 expect(body_as_json[0][:id]).to eq alice.id.to_s end + + context 'when requesting user is blocked' do + before do + account.block!(user.account) + end + + it 'hides results' do + get :index, params: { account_id: account.id, limit: 2 } + expect(body_as_json.size).to eq 0 + end + end + + context 'when requesting user is the account owner' do + let(:user) { Fabricate(:user, account: account) } + + it 'returns all accounts, including muted accounts' do + user.account.mute!(bob) + get :index, params: { account_id: account.id, limit: 2 } + + expect(body_as_json.size).to eq 2 + expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s]) + end + end end end -- cgit From 8be4c2ba21c6a8e4abb0522dac398645c71d8e94 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 10 May 2020 11:21:10 +0200 Subject: Add ability to remove identity proofs from account (#13682) Fix #12613 --- .../settings/identity_proofs_controller.rb | 12 ++++++++---- app/views/settings/identity_proofs/_proof.html.haml | 1 + config/locales/en.yml | 4 +++- config/routes.rb | 2 +- .../settings/identity_proofs_controller_spec.rb | 20 +++++++++++++++++++- 5 files changed, 32 insertions(+), 7 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb index a749d8020..3a90b7c4d 100644 --- a/app/controllers/settings/identity_proofs_controller.rb +++ b/app/controllers/settings/identity_proofs_controller.rb @@ -21,8 +21,7 @@ class Settings::IdentityProofsController < Settings::BaseController if current_account.username.casecmp(params[:username]).zero? render layout: 'auth' else - flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username) - redirect_to settings_identity_proofs_path + redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username) end end @@ -34,11 +33,16 @@ class Settings::IdentityProofsController < Settings::BaseController PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof? redirect_to @proof.on_success_path(params[:user_agent]) else - flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize) - redirect_to settings_identity_proofs_path + redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize) end end + def destroy + @proof = current_account.identity_proofs.find(params[:id]) + @proof.destroy! + redirect_to settings_identity_proofs_path, success: I18n.t('identity_proofs.removed') + end + private def check_required_params diff --git a/app/views/settings/identity_proofs/_proof.html.haml b/app/views/settings/identity_proofs/_proof.html.haml index 524827ad7..14e8e91be 100644 --- a/app/views/settings/identity_proofs/_proof.html.haml +++ b/app/views/settings/identity_proofs/_proof.html.haml @@ -18,3 +18,4 @@ %td = table_link_to 'external-link', t('identity_proofs.view_proof'), proof.badge.proof_url if proof.badge.proof_url + = table_link_to 'trash', t('identity_proofs.remove'), settings_identity_proof_path(proof), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } diff --git a/config/locales/en.yml b/config/locales/en.yml index 8a7cf070f..cc34b9094 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -844,12 +844,14 @@ en: invalid_token: Keybase tokens are hashes of signatures and must be 66 hex characters verification_failed: Keybase does not recognize this token as a signature of Keybase user %{kb_username}. Please retry from Keybase. wrong_user: Cannot create a proof for %{proving} while logged in as %{current}. Log in as %{proving} and try again. - explanation_html: Here you can cryptographically connect your other identities, such as a Keybase profile. This lets other people send you encrypted messages and trust content you send them. + explanation_html: Here you can cryptographically connect your other identities from other platforms, such as Keybase. This lets other people send you encrypted messages on those platforms and allows them to trust that the content you send them comes from you. i_am_html: I am %{username} on %{service}. identity: Identity inactive: Inactive publicize_checkbox: 'And toot this:' publicize_toot: 'It is proven! I am %{username} on %{service}: %{url}' + remove: Remove proof from account + removed: Successfully removed proof from account status: Verification status view_proof: View proof imports: diff --git a/config/routes.rb b/config/routes.rb index fa6639138..920a48fe7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -130,7 +130,7 @@ Rails.application.routes.draw do resource :confirmation, only: [:new, :create] end - resources :identity_proofs, only: [:index, :show, :new, :create, :update] + resources :identity_proofs, only: [:index, :new, :create, :destroy] resources :applications, except: [:edit] do member do diff --git a/spec/controllers/settings/identity_proofs_controller_spec.rb b/spec/controllers/settings/identity_proofs_controller_spec.rb index 261e980d4..16f236227 100644 --- a/spec/controllers/settings/identity_proofs_controller_spec.rb +++ b/spec/controllers/settings/identity_proofs_controller_spec.rb @@ -151,7 +151,7 @@ describe Settings::IdentityProofsController do @proof1 = Fabricate(:account_identity_proof, account: user.account) @proof2 = Fabricate(:account_identity_proof, account: user.account) allow_any_instance_of(AccountIdentityProof).to receive(:badge) { double(avatar_url: '', profile_url: '', proof_url: '') } - allow_any_instance_of(AccountIdentityProof).to receive(:refresh!) { } + allow_any_instance_of(AccountIdentityProof).to receive(:refresh!) {} end it 'has the first proof username on the page' do @@ -165,4 +165,22 @@ describe Settings::IdentityProofsController do end end end + + describe 'DELETE #destroy' do + before do + allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { true } + @proof1 = Fabricate(:account_identity_proof, account: user.account) + allow_any_instance_of(AccountIdentityProof).to receive(:badge) { double(avatar_url: '', profile_url: '', proof_url: '') } + allow_any_instance_of(AccountIdentityProof).to receive(:refresh!) {} + delete :destroy, params: { id: @proof1.id } + end + + it 'redirects to :index' do + expect(response).to redirect_to settings_identity_proofs_path + end + + it 'removes the proof' do + expect(AccountIdentityProof.where(id: @proof1.id).count).to eq 0 + end + end end -- cgit From 4b766f984689311523b89e1b68d2a11dff3fc396 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 10 May 2020 11:41:43 +0200 Subject: Refactor monkey-patching of Goldfinger (#12561) --- app/helpers/webfinger_helper.rb | 19 +++++++++++++++++++ app/models/remote_follow.rb | 3 ++- .../activitypub/fetch_remote_account_service.rb | 5 +++-- app/services/resolve_account_service.rb | 3 ++- config/initializers/http_client_proxy.rb | 20 +++++++++----------- spec/controllers/remote_follow_controller_spec.rb | 10 +++++----- 6 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 app/helpers/webfinger_helper.rb (limited to 'spec/controllers') diff --git a/app/helpers/webfinger_helper.rb b/app/helpers/webfinger_helper.rb new file mode 100644 index 000000000..70c493210 --- /dev/null +++ b/app/helpers/webfinger_helper.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module WebfingerHelper + def webfinger!(uri) + hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri) + + raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && hidden_service_uri + + opts = { + ssl: !hidden_service_uri, + + headers: { + 'User-Agent': Mastodon::Version.user_agent, + }, + } + + Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger + end +end diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb index 5ea535287..30b84f7d5 100644 --- a/app/models/remote_follow.rb +++ b/app/models/remote_follow.rb @@ -3,6 +3,7 @@ class RemoteFollow include ActiveModel::Validations include RoutingHelper + include WebfingerHelper attr_accessor :acct, :addressable_template @@ -71,7 +72,7 @@ class RemoteFollow end def acct_resource - @acct_resource ||= Goldfinger.finger("acct:#{acct}") + @acct_resource ||= webfinger!("acct:#{acct}") rescue Goldfinger::Error, HTTP::ConnectionError nil end diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb index d65c8f951..83fbf6d07 100644 --- a/app/services/activitypub/fetch_remote_account_service.rb +++ b/app/services/activitypub/fetch_remote_account_service.rb @@ -3,6 +3,7 @@ class ActivityPub::FetchRemoteAccountService < BaseService include JsonLdHelper include DomainControlHelper + include WebfingerHelper SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze @@ -35,12 +36,12 @@ class ActivityPub::FetchRemoteAccountService < BaseService private def verified_webfinger? - webfinger = Goldfinger.finger("acct:#{@username}@#{@domain}") + webfinger = webfinger!("acct:#{@username}@#{@domain}") confirmed_username, confirmed_domain = split_acct(webfinger.subject) return webfinger.link('self')&.href == @uri if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero? - webfinger = Goldfinger.finger("acct:#{confirmed_username}@#{confirmed_domain}") + webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}") @username, @domain = split_acct(webfinger.subject) self_reference = webfinger.link('self') diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index 1ad9ed407..17ace100c 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -3,6 +3,7 @@ class ResolveAccountService < BaseService include JsonLdHelper include DomainControlHelper + include WebfingerHelper class WebfingerRedirectError < StandardError; end @@ -76,7 +77,7 @@ class ResolveAccountService < BaseService end def process_webfinger!(uri, redirected = false) - @webfinger = Goldfinger.finger("acct:#{uri}") + @webfinger = webfinger!("acct:#{uri}") confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@') if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero? diff --git a/config/initializers/http_client_proxy.rb b/config/initializers/http_client_proxy.rb index 9d7b16e69..7a9b7b86d 100644 --- a/config/initializers/http_client_proxy.rb +++ b/config/initializers/http_client_proxy.rb @@ -1,24 +1,22 @@ Rails.application.configure do config.x.http_client_proxy = {} + if ENV['http_proxy'].present? proxy = URI.parse(ENV['http_proxy']) + raise "Unsupported proxy type: #{proxy.scheme}" unless %w(http https).include? proxy.scheme raise "No proxy host" unless proxy.host host = proxy.host host = host[1...-1] if host[0] == '[' # for IPv6 address - config.x.http_client_proxy[:proxy] = { proxy_address: host, proxy_port: proxy.port, proxy_username: proxy.user, proxy_password: proxy.password }.compact + + config.x.http_client_proxy[:proxy] = { + proxy_address: host, + proxy_port: proxy.port, + proxy_username: proxy.user, + proxy_password: proxy.password, + }.compact end config.x.access_to_hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true' end - -module Goldfinger - def self.finger(uri, opts = {}) - to_hidden = /\.(onion|i2p)(:\d+)?$/.match(uri) - raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && to_hidden - opts = { ssl: !to_hidden, headers: {} }.merge(Rails.configuration.x.http_client_proxy).merge(opts) - opts[:headers]['User-Agent'] ||= Mastodon::Version.user_agent - Goldfinger::Client.new(uri, opts).finger - end -end diff --git a/spec/controllers/remote_follow_controller_spec.rb b/spec/controllers/remote_follow_controller_spec.rb index d79dd2949..3ef8f14d9 100644 --- a/spec/controllers/remote_follow_controller_spec.rb +++ b/spec/controllers/remote_follow_controller_spec.rb @@ -35,7 +35,7 @@ describe RemoteFollowController do context 'when webfinger values are wrong' do it 'renders new when redirect url is nil' do resource_with_nil_link = double(link: nil) - allow(Goldfinger).to receive(:finger).with('acct:user@example.com').and_return(resource_with_nil_link) + allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_nil_link) post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } } expect(response).to render_template(:new) @@ -45,7 +45,7 @@ describe RemoteFollowController do it 'renders new when template is nil' do link_with_nil_template = double(template: nil) resource_with_link = double(link: link_with_nil_template) - allow(Goldfinger).to receive(:finger).with('acct:user@example.com').and_return(resource_with_link) + allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link) post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } } expect(response).to render_template(:new) @@ -57,7 +57,7 @@ describe RemoteFollowController do before do link_with_template = double(template: 'http://example.com/follow_me?acct={uri}') resource_with_link = double(link: link_with_template) - allow(Goldfinger).to receive(:finger).with('acct:user@example.com').and_return(resource_with_link) + allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link) post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } } end @@ -79,7 +79,7 @@ describe RemoteFollowController do end it 'renders new with error when goldfinger fails' do - allow(Goldfinger).to receive(:finger).with('acct:user@example.com').and_raise(Goldfinger::Error) + allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_raise(Goldfinger::Error) post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } } expect(response).to render_template(:new) @@ -87,7 +87,7 @@ describe RemoteFollowController do end it 'renders new when occur HTTP::ConnectionError' do - allow(Goldfinger).to receive(:finger).with('acct:user@unknown').and_raise(HTTP::ConnectionError) + allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@unknown').and_raise(HTTP::ConnectionError) post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@unknown' } } expect(response).to render_template(:new) -- cgit