From 3de91456132ffadf5b98848409fa2a0377a3bef6 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 15 Nov 2023 14:12:02 -0500 Subject: [PATCH] Move controller specs for `well-known` endpoints to request specs (#27855) --- .../well_known/host_meta_controller_spec.rb | 22 -- .../well_known/node_info_controller_spec.rb | 41 --- .../well_known/webfinger_controller_spec.rb | 235 ---------------- spec/requests/host_meta_request_spec.rb | 14 - spec/requests/webfinger_request_spec.rb | 33 --- spec/requests/well_known/host_meta_spec.rb | 27 ++ spec/requests/well_known/node_info_spec.rb | 58 ++++ spec/requests/well_known/webfinger_spec.rb | 255 ++++++++++++++++++ 8 files changed, 340 insertions(+), 345 deletions(-) delete mode 100644 spec/controllers/well_known/host_meta_controller_spec.rb delete mode 100644 spec/controllers/well_known/node_info_controller_spec.rb delete mode 100644 spec/controllers/well_known/webfinger_controller_spec.rb delete mode 100644 spec/requests/host_meta_request_spec.rb delete mode 100644 spec/requests/webfinger_request_spec.rb create mode 100644 spec/requests/well_known/host_meta_spec.rb create mode 100644 spec/requests/well_known/node_info_spec.rb create mode 100644 spec/requests/well_known/webfinger_spec.rb diff --git a/spec/controllers/well_known/host_meta_controller_spec.rb b/spec/controllers/well_known/host_meta_controller_spec.rb deleted file mode 100644 index 4bd161cd9..000000000 --- a/spec/controllers/well_known/host_meta_controller_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe WellKnown::HostMetaController do - render_views - - describe 'GET #show' do - it 'returns http success' do - get :show, format: :xml - - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/xrd+xml' - expect(response.body).to eq <<~XML - - - - - XML - end - end -end diff --git a/spec/controllers/well_known/node_info_controller_spec.rb b/spec/controllers/well_known/node_info_controller_spec.rb deleted file mode 100644 index 6ec34afd0..000000000 --- a/spec/controllers/well_known/node_info_controller_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe WellKnown::NodeInfoController do - render_views - - describe 'GET #index' do - it 'returns json document pointing to node info' do - get :index - - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/json' - - json = body_as_json - - expect(json[:links]).to be_an Array - expect(json[:links][0][:rel]).to eq 'http://nodeinfo.diaspora.software/ns/schema/2.0' - expect(json[:links][0][:href]).to include 'nodeinfo/2.0' - end - end - - describe 'GET #show' do - it 'returns json document with node info properties' do - get :show - - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/json' - - json = body_as_json - foo = { 'foo' => 0 } - - expect(foo).to_not match_json_schema('nodeinfo_2.0') - expect(json).to match_json_schema('nodeinfo_2.0') - expect(json[:version]).to eq '2.0' - expect(json[:usage]).to be_a Hash - expect(json[:software]).to be_a Hash - expect(json[:protocols]).to be_an Array - end - end -end diff --git a/spec/controllers/well_known/webfinger_controller_spec.rb b/spec/controllers/well_known/webfinger_controller_spec.rb deleted file mode 100644 index 6610f4d13..000000000 --- a/spec/controllers/well_known/webfinger_controller_spec.rb +++ /dev/null @@ -1,235 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe WellKnown::WebfingerController do - include RoutingHelper - - render_views - - describe 'GET #show' do - subject(:perform_show!) do - get :show, params: { resource: resource }, format: :json - end - - let(:alternate_domains) { [] } - let(:alice) { Fabricate(:account, username: 'alice') } - let(:resource) { nil } - - around do |example| - tmp = Rails.configuration.x.alternate_domains - Rails.configuration.x.alternate_domains = alternate_domains - example.run - Rails.configuration.x.alternate_domains = tmp - end - - shared_examples 'a successful response' do - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'does not set a Vary header' do - expect(response.headers['Vary']).to be_nil - end - - it 'returns application/jrd+json' do - expect(response.media_type).to eq 'application/jrd+json' - end - - it 'returns links for the account' do - json = body_as_json - expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io' - expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice') - end - end - - context 'when an account exists' do - let(:resource) { alice.to_webfinger_s } - - before do - perform_show! - end - - it_behaves_like 'a successful response' - end - - context 'when an account is temporarily suspended' do - let(:resource) { alice.to_webfinger_s } - - before do - alice.suspend! - perform_show! - end - - it_behaves_like 'a successful response' - end - - context 'when an account is permanently suspended or deleted' do - let(:resource) { alice.to_webfinger_s } - - before do - alice.suspend! - alice.deletion_request.destroy - perform_show! - end - - it 'returns http gone' do - expect(response).to have_http_status(410) - end - end - - context 'when an account is not found' do - let(:resource) { 'acct:not@existing.com' } - - before do - perform_show! - end - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - - context 'with an alternate domain' do - let(:alternate_domains) { ['foo.org'] } - - before do - perform_show! - end - - context 'when an account exists' do - let(:resource) do - username, = alice.to_webfinger_s.split('@') - "#{username}@foo.org" - end - - it_behaves_like 'a successful response' - end - - context 'when the domain is wrong' do - let(:resource) do - username, = alice.to_webfinger_s.split('@') - "#{username}@bar.org" - end - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end - - context 'when the old name scheme is used to query the instance actor' do - let(:resource) do - "#{Rails.configuration.x.local_domain}@#{Rails.configuration.x.local_domain}" - end - - before do - perform_show! - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'does not set a Vary header' do - expect(response.headers['Vary']).to be_nil - end - - it 'returns application/jrd+json' do - expect(response.media_type).to eq 'application/jrd+json' - end - - it 'returns links for the internal account' do - json = body_as_json - expect(json[:subject]).to eq 'acct:mastodon.internal@cb6e6126.ngrok.io' - expect(json[:aliases]).to eq ['https://cb6e6126.ngrok.io/actor'] - end - end - - context 'with no resource parameter' do - let(:resource) { nil } - - before do - perform_show! - end - - it 'returns http bad request' do - expect(response).to have_http_status(400) - end - end - - context 'with a nonsense parameter' do - let(:resource) { 'df/:dfkj' } - - before do - perform_show! - end - - it 'returns http bad request' do - expect(response).to have_http_status(400) - end - end - - context 'when an account has an avatar' do - let(:alice) { Fabricate(:account, username: 'alice', avatar: attachment_fixture('attachment.jpg')) } - let(:resource) { alice.to_webfinger_s } - - it 'returns avatar in response' do - perform_show! - - avatar_link = get_avatar_link(body_as_json) - expect(avatar_link).to_not be_nil - expect(avatar_link[:type]).to eq alice.avatar.content_type - expect(avatar_link[:href]).to eq full_asset_url(alice.avatar) - end - - context 'with limited federation mode' do - before do - allow(Rails.configuration.x).to receive(:limited_federation_mode).and_return(true) - end - - it 'does not return avatar in response' do - perform_show! - - avatar_link = get_avatar_link(body_as_json) - expect(avatar_link).to be_nil - end - end - - context 'when enabling DISALLOW_UNAUTHENTICATED_API_ACCESS' do - around do |example| - ClimateControl.modify DISALLOW_UNAUTHENTICATED_API_ACCESS: 'true' do - example.run - end - end - - it 'does not return avatar in response' do - perform_show! - - avatar_link = get_avatar_link(body_as_json) - expect(avatar_link).to be_nil - end - end - end - - context 'when an account does not have an avatar' do - let(:alice) { Fabricate(:account, username: 'alice', avatar: nil) } - let(:resource) { alice.to_webfinger_s } - - before do - perform_show! - end - - it 'does not return avatar in response' do - avatar_link = get_avatar_link(body_as_json) - expect(avatar_link).to be_nil - end - end - end - - private - - def get_avatar_link(json) - json[:links].find { |link| link[:rel] == 'http://webfinger.net/rel/avatar' } - end -end diff --git a/spec/requests/host_meta_request_spec.rb b/spec/requests/host_meta_request_spec.rb deleted file mode 100644 index ec26ecba7..000000000 --- a/spec/requests/host_meta_request_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'The host_meta route' do - describe 'requested without accepts headers' do - it 'returns an xml response' do - get host_meta_url - - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/xrd+xml' - end - end -end diff --git a/spec/requests/webfinger_request_spec.rb b/spec/requests/webfinger_request_spec.rb deleted file mode 100644 index 68a1478be..000000000 --- a/spec/requests/webfinger_request_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'The webfinger route' do - let(:alice) { Fabricate(:account, username: 'alice') } - - describe 'requested with standard accepts headers' do - it 'returns a json response' do - get webfinger_url(resource: alice.to_webfinger_s) - - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/jrd+json' - end - end - - describe 'asking for json format' do - it 'returns a json response for json format' do - get webfinger_url(resource: alice.to_webfinger_s, format: :json) - - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/jrd+json' - end - - it 'returns a json response for json accept header' do - headers = { 'HTTP_ACCEPT' => 'application/jrd+json' } - get webfinger_url(resource: alice.to_webfinger_s), headers: headers - - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/jrd+json' - end - end -end diff --git a/spec/requests/well_known/host_meta_spec.rb b/spec/requests/well_known/host_meta_spec.rb new file mode 100644 index 000000000..ca10a51a0 --- /dev/null +++ b/spec/requests/well_known/host_meta_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'The /.well-known/host-meta request' do + it 'returns http success with valid XML response' do + get '/.well-known/host-meta' + + expect(response) + .to have_http_status(200) + .and have_attributes( + media_type: 'application/xrd+xml', + body: host_meta_xml_template + ) + end + + private + + def host_meta_xml_template + <<~XML + + + + + XML + end +end diff --git a/spec/requests/well_known/node_info_spec.rb b/spec/requests/well_known/node_info_spec.rb new file mode 100644 index 000000000..0934b0fde --- /dev/null +++ b/spec/requests/well_known/node_info_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'The well-known node-info endpoints' do + describe 'The /.well-known/node-info endpoint' do + it 'returns JSON document pointing to node info' do + get '/.well-known/nodeinfo' + + expect(response) + .to have_http_status(200) + .and have_attributes( + media_type: 'application/json' + ) + + expect(body_as_json).to include( + links: be_an(Array).and( + contain_exactly( + include( + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', + href: include('nodeinfo/2.0') + ) + ) + ) + ) + end + end + + describe 'The /nodeinfo/2.0 endpoint' do + it 'returns JSON document with node info properties' do + get '/nodeinfo/2.0' + + expect(response) + .to have_http_status(200) + .and have_attributes( + media_type: 'application/json' + ) + + expect(non_matching_hash) + .to_not match_json_schema('nodeinfo_2.0') + + expect(body_as_json) + .to match_json_schema('nodeinfo_2.0') + .and include( + version: '2.0', + usage: be_a(Hash), + software: be_a(Hash), + protocols: be_a(Array) + ) + end + + private + + def non_matching_hash + { 'foo' => 0 } + end + end +end diff --git a/spec/requests/well_known/webfinger_spec.rb b/spec/requests/well_known/webfinger_spec.rb new file mode 100644 index 000000000..779f1bba5 --- /dev/null +++ b/spec/requests/well_known/webfinger_spec.rb @@ -0,0 +1,255 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'The /.well-known/webfinger endpoint' do + subject(:perform_request!) { get webfinger_url(resource: resource) } + + let(:alternate_domains) { [] } + let(:alice) { Fabricate(:account, username: 'alice') } + let(:resource) { nil } + + around do |example| + tmp = Rails.configuration.x.alternate_domains + Rails.configuration.x.alternate_domains = alternate_domains + example.run + Rails.configuration.x.alternate_domains = tmp + end + + shared_examples 'a successful response' do + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'sets only a Vary Origin header' do + expect(response.headers['Vary']).to eq('Origin') + end + + it 'returns application/jrd+json' do + expect(response.media_type).to eq 'application/jrd+json' + end + + it 'returns links for the account' do + json = body_as_json + expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io' + expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice') + end + end + + context 'when an account exists' do + let(:resource) { alice.to_webfinger_s } + + before do + perform_request! + end + + it_behaves_like 'a successful response' + end + + context 'when an account is temporarily suspended' do + let(:resource) { alice.to_webfinger_s } + + before do + alice.suspend! + perform_request! + end + + it_behaves_like 'a successful response' + end + + context 'when an account is permanently suspended or deleted' do + let(:resource) { alice.to_webfinger_s } + + before do + alice.suspend! + alice.deletion_request.destroy + perform_request! + end + + it 'returns http gone' do + expect(response).to have_http_status(410) + end + end + + context 'when an account is not found' do + let(:resource) { 'acct:not@existing.com' } + + before do + perform_request! + end + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'with an alternate domain' do + let(:alternate_domains) { ['foo.org'] } + + before do + perform_request! + end + + context 'when an account exists' do + let(:resource) do + username, = alice.to_webfinger_s.split('@') + "#{username}@foo.org" + end + + it_behaves_like 'a successful response' + end + + context 'when the domain is wrong' do + let(:resource) do + username, = alice.to_webfinger_s.split('@') + "#{username}@bar.org" + end + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the old name scheme is used to query the instance actor' do + let(:resource) do + "#{Rails.configuration.x.local_domain}@#{Rails.configuration.x.local_domain}" + end + + before do + perform_request! + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'sets only a Vary Origin header' do + expect(response.headers['Vary']).to eq('Origin') + end + + it 'returns application/jrd+json' do + expect(response.media_type).to eq 'application/jrd+json' + end + + it 'returns links for the internal account' do + json = body_as_json + expect(json[:subject]).to eq 'acct:mastodon.internal@cb6e6126.ngrok.io' + expect(json[:aliases]).to eq ['https://cb6e6126.ngrok.io/actor'] + end + end + + context 'with no resource parameter' do + let(:resource) { nil } + + before do + perform_request! + end + + it 'returns http bad request' do + expect(response).to have_http_status(400) + end + end + + context 'with a nonsense parameter' do + let(:resource) { 'df/:dfkj' } + + before do + perform_request! + end + + it 'returns http bad request' do + expect(response).to have_http_status(400) + end + end + + context 'when an account has an avatar' do + let(:alice) { Fabricate(:account, username: 'alice', avatar: attachment_fixture('attachment.jpg')) } + let(:resource) { alice.to_webfinger_s } + + it 'returns avatar in response' do + perform_request! + + avatar_link = get_avatar_link(body_as_json) + expect(avatar_link).to_not be_nil + expect(avatar_link[:type]).to eq alice.avatar.content_type + expect(avatar_link[:href]).to eq Addressable::URI.new(host: Rails.configuration.x.local_domain, path: alice.avatar.to_s, scheme: 'https').to_s + end + + context 'with limited federation mode' do + before do + allow(Rails.configuration.x).to receive(:limited_federation_mode).and_return(true) + end + + it 'does not return avatar in response' do + perform_request! + + avatar_link = get_avatar_link(body_as_json) + expect(avatar_link).to be_nil + end + end + + context 'when enabling DISALLOW_UNAUTHENTICATED_API_ACCESS' do + around do |example| + ClimateControl.modify DISALLOW_UNAUTHENTICATED_API_ACCESS: 'true' do + example.run + end + end + + it 'does not return avatar in response' do + perform_request! + + avatar_link = get_avatar_link(body_as_json) + expect(avatar_link).to be_nil + end + end + end + + context 'when an account does not have an avatar' do + let(:alice) { Fabricate(:account, username: 'alice', avatar: nil) } + let(:resource) { alice.to_webfinger_s } + + before do + perform_request! + end + + it 'does not return avatar in response' do + avatar_link = get_avatar_link(body_as_json) + expect(avatar_link).to be_nil + end + end + + context 'with different headers' do + describe 'requested with standard accepts headers' do + it 'returns a json response' do + get webfinger_url(resource: alice.to_webfinger_s) + + expect(response).to have_http_status(200) + expect(response.media_type).to eq 'application/jrd+json' + end + end + + describe 'asking for json format' do + it 'returns a json response for json format' do + get webfinger_url(resource: alice.to_webfinger_s, format: :json) + + expect(response).to have_http_status(200) + expect(response.media_type).to eq 'application/jrd+json' + end + + it 'returns a json response for json accept header' do + headers = { 'HTTP_ACCEPT' => 'application/jrd+json' } + get webfinger_url(resource: alice.to_webfinger_s), headers: headers + + expect(response).to have_http_status(200) + expect(response.media_type).to eq 'application/jrd+json' + end + end + end + + private + + def get_avatar_link(json) + json[:links].find { |link| link[:rel] == 'http://webfinger.net/rel/avatar' } + end +end