Merge commit '0530ce5e9575c09464847412f43852f438b0494e' into glitch-soc/merge-upstream

main
Claire 2023-12-20 21:58:18 +01:00
commit 8a5f2442cc
5 changed files with 166 additions and 98 deletions

View File

@ -8,6 +8,11 @@ class Api::V2::SearchController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:search' } before_action -> { authorize_if_got_token! :read, :'read:search' }
before_action :validate_search_params! before_action :validate_search_params!
with_options unless: :user_signed_in? do
before_action :query_pagination_error, if: :pagination_requested?
before_action :remote_resolve_error, if: :remote_resolve_requested?
end
def index def index
@search = Search.new(search_results) @search = Search.new(search_results)
render json: @search, serializer: REST::SearchSerializer render json: @search, serializer: REST::SearchSerializer
@ -21,12 +26,22 @@ class Api::V2::SearchController < Api::BaseController
def validate_search_params! def validate_search_params!
params.require(:q) params.require(:q)
end
return if user_signed_in? def query_pagination_error
render json: { error: 'Search queries pagination is not supported without authentication' }, status: 401
end
return render json: { error: 'Search queries pagination is not supported without authentication' }, status: 401 if params[:offset].present? def remote_resolve_error
render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401
end
render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401 if truthy_param?(:resolve) def remote_resolve_requested?
truthy_param?(:resolve)
end
def pagination_requested?
params[:offset].present?
end end
def search_results def search_results
@ -34,7 +49,15 @@ class Api::V2::SearchController < Api::BaseController
params[:q], params[:q],
current_account, current_account,
limit_param(RESULTS_LIMIT), limit_param(RESULTS_LIMIT),
search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed), following: truthy_param?(:following)) combined_search_params
)
end
def combined_search_params
search_params.merge(
resolve: truthy_param?(:resolve),
exclude_unreviewed: truthy_param?(:exclude_unreviewed),
following: truthy_param?(:following)
) )
end end

View File

@ -2732,22 +2732,16 @@ $ui-header-height: 55px;
&__description { &__description {
flex: 1 1 auto; flex: 1 1 auto;
line-height: 20px; line-height: 20px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
h6 { h6 {
color: $highlight-text-color; color: $highlight-text-color;
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
} }
p { p {
color: $darker-text-color; color: $darker-text-color;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
} }
} }
} }

View File

@ -13,21 +13,22 @@
# end # end
ActiveSupport::Inflector.inflections(:en) do |inflect| ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'StatsD'
inflect.acronym 'OEmbed'
inflect.acronym 'OStatus'
inflect.acronym 'ActivityPub' inflect.acronym 'ActivityPub'
inflect.acronym 'PubSubHubbub'
inflect.acronym 'ActivityStreams' inflect.acronym 'ActivityStreams'
inflect.acronym 'JsonLd'
inflect.acronym 'Ed25519'
inflect.acronym 'TOC'
inflect.acronym 'RSS'
inflect.acronym 'REST'
inflect.acronym 'URL'
inflect.acronym 'ASCII' inflect.acronym 'ASCII'
inflect.acronym 'CLI'
inflect.acronym 'DeepL' inflect.acronym 'DeepL'
inflect.acronym 'DSL' inflect.acronym 'DSL'
inflect.acronym 'Ed25519'
inflect.acronym 'JsonLd'
inflect.acronym 'OEmbed'
inflect.acronym 'OStatus'
inflect.acronym 'PubSubHubbub'
inflect.acronym 'REST'
inflect.acronym 'RSS'
inflect.acronym 'StatsD'
inflect.acronym 'TOC'
inflect.acronym 'URL'
inflect.singular 'data', 'data' inflect.singular 'data', 'data'
end end

View File

@ -34,6 +34,26 @@ RSpec.describe Api::V2::SearchController do
expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(bob.id.to_s, ana.id.to_s, tom.id.to_s) expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(bob.id.to_s, ana.id.to_s, tom.id.to_s)
end end
context 'with truthy `resolve`' do
let(:params) { { q: 'test1', resolve: '1' } }
it 'returns http unauthorized' do
get :index, params: params
expect(response).to have_http_status(200)
end
end
context 'with `offset`' do
let(:params) { { q: 'test1', offset: 1 } }
it 'returns http unauthorized' do
get :index, params: params
expect(response).to have_http_status(200)
end
end
context 'with following=true' do context 'with following=true' do
let(:params) { { q: 'test', type: 'accounts', following: 'true' } } let(:params) { { q: 'test', type: 'accounts', following: 'true' } }
@ -48,6 +68,26 @@ RSpec.describe Api::V2::SearchController do
end end
end end
end end
context 'when search raises syntax error' do
before { allow(Search).to receive(:new).and_raise(Mastodon::SyntaxError) }
it 'returns http unprocessable_entity' do
get :index, params: params
expect(response).to have_http_status(422)
end
end
context 'when search raises not found error' do
before { allow(Search).to receive(:new).and_raise(ActiveRecord::RecordNotFound) }
it 'returns http not_found' do
get :index, params: params
expect(response).to have_http_status(404)
end
end
end end
end end
@ -59,6 +99,12 @@ RSpec.describe Api::V2::SearchController do
get :index, params: search_params get :index, params: search_params
end end
context 'without a `q` param' do
it 'returns http bad_request' do
expect(response).to have_http_status(400)
end
end
context 'with a `q` shorter than 5 characters' do context 'with a `q` shorter than 5 characters' do
let(:search_params) { { q: 'test' } } let(:search_params) { { q: 'test' } }
@ -79,6 +125,7 @@ RSpec.describe Api::V2::SearchController do
it 'returns http unauthorized' do it 'returns http unauthorized' do
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
expect(response.body).to match('resolve remote resources')
end end
end end
@ -87,6 +134,7 @@ RSpec.describe Api::V2::SearchController do
it 'returns http unauthorized' do it 'returns http unauthorized' do
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
expect(response.body).to match('pagination is not supported')
end end
end end
end end

View File

@ -2,23 +2,22 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe AccountsController do describe 'Accounts show response' do
render_views
let(:account) { Fabricate(:account) } let(:account) { Fabricate(:account) }
describe 'unapproved account check' do context 'with an unapproved account' do
before { account.user.update(approved: false) } before { account.user.update(approved: false) }
it 'returns http not found' do it 'returns http not found' do
%w(html json rss).each do |format| %w(html json rss).each do |format|
get :show, params: { username: account.username, format: format } get short_account_path(username: account.username), as: format
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
end end
end end
describe 'permanently suspended account check' do context 'with a permanently suspended account' do
before do before do
account.suspend! account.suspend!
account.deletion_request.destroy account.deletion_request.destroy
@ -26,25 +25,26 @@ RSpec.describe AccountsController do
it 'returns http gone' do it 'returns http gone' do
%w(html json rss).each do |format| %w(html json rss).each do |format|
get :show, params: { username: account.username, format: format } get short_account_path(username: account.username), as: format
expect(response).to have_http_status(410) expect(response).to have_http_status(410)
end end
end end
end end
describe 'temporarily suspended account check' do context 'with a temporarily suspended account' do
before { account.suspend! } before { account.suspend! }
it 'returns appropriate http response code' do it 'returns appropriate http response code' do
{ html: 403, json: 200, rss: 403 }.each do |format, code| { html: 403, json: 200, rss: 403 }.each do |format, code|
get :show, params: { username: account.username, format: format } get short_account_path(username: account.username), as: format
expect(response).to have_http_status(code) expect(response).to have_http_status(code)
end end
end end
end end
describe 'GET #show' do describe 'GET to short username paths' do
context 'with existing statuses' do context 'with existing statuses' do
let!(:status) { Fabricate(:status, account: account) } let!(:status) { Fabricate(:status, account: account) }
let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) } let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) }
@ -66,17 +66,17 @@ RSpec.describe AccountsController do
shared_examples 'common HTML response' do shared_examples 'common HTML response' do
it 'returns a standard HTML response', :aggregate_failures do it 'returns a standard HTML response', :aggregate_failures do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
.and render_template(:show)
expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account) expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account)
expect(response).to render_template(:show)
end end
end end
context 'with a normal account in an HTML request' do context 'with a normal account in an HTML request' do
before do before do
get :show, params: { username: account.username, format: format } get short_account_path(username: account.username), as: format
end end
it_behaves_like 'common HTML response' it_behaves_like 'common HTML response'
@ -84,8 +84,7 @@ RSpec.describe AccountsController do
context 'with replies' do context 'with replies' do
before do before do
allow(controller).to receive(:replies_requested?).and_return(true) get short_account_with_replies_path(username: account.username), as: format
get :show, params: { username: account.username, format: format }
end end
it_behaves_like 'common HTML response' it_behaves_like 'common HTML response'
@ -93,8 +92,7 @@ RSpec.describe AccountsController do
context 'with media' do context 'with media' do
before do before do
allow(controller).to receive(:media_requested?).and_return(true) get short_account_media_path(username: account.username), as: format
get :show, params: { username: account.username, format: format }
end end
it_behaves_like 'common HTML response' it_behaves_like 'common HTML response'
@ -106,9 +104,8 @@ RSpec.describe AccountsController do
let!(:status_tag) { Fabricate(:status, account: account) } let!(:status_tag) { Fabricate(:status, account: account) }
before do before do
allow(controller).to receive(:tag_requested?).and_return(true)
status_tag.tags << tag status_tag.tags << tag
get :show, params: { username: account.username, format: format, tag: tag.to_param } get short_account_tag_path(username: account.username, tag: tag), as: format
end end
it_behaves_like 'common HTML response' it_behaves_like 'common HTML response'
@ -117,21 +114,25 @@ RSpec.describe AccountsController do
context 'with JSON' do context 'with JSON' do
let(:authorized_fetch_mode) { false } let(:authorized_fetch_mode) { false }
let(:format) { 'json' } let(:headers) { { 'ACCEPT' => 'application/json' } }
before do around do |example|
allow(controller).to receive(:authorized_fetch_mode?).and_return(authorized_fetch_mode) ClimateControl.modify AUTHORIZED_FETCH: authorized_fetch_mode.to_s do
example.run
end
end end
context 'with a normal account in a JSON request' do context 'with a normal account in a JSON request' do
before do before do
get :show, params: { username: account.username, format: format } get short_account_path(username: account.username), headers: headers
end end
it 'returns a JSON version of the account', :aggregate_failures do it 'returns a JSON version of the account', :aggregate_failures do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
expect(response.media_type).to eq 'application/activity+json' .and have_attributes(
media_type: eq('application/activity+json')
)
expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end end
@ -152,13 +153,15 @@ RSpec.describe AccountsController do
before do before do
sign_in(user) sign_in(user)
get :show, params: { username: account.username, format: format } get short_account_path(username: account.username), headers: headers.merge({ 'Cookie' => '123' })
end end
it 'returns a private JSON version of the account', :aggregate_failures do it 'returns a private JSON version of the account', :aggregate_failures do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
expect(response.media_type).to eq 'application/activity+json' .and have_attributes(
media_type: eq('application/activity+json')
)
expect(response.headers['Cache-Control']).to include 'private' expect(response.headers['Cache-Control']).to include 'private'
@ -170,14 +173,15 @@ RSpec.describe AccountsController do
let(:remote_account) { Fabricate(:account, domain: 'example.com') } let(:remote_account) { Fabricate(:account, domain: 'example.com') }
before do before do
allow(controller).to receive(:signed_request_actor).and_return(remote_account) get short_account_path(username: account.username), headers: headers, sign_with: remote_account
get :show, params: { username: account.username, format: format }
end end
it 'returns a JSON version of the account', :aggregate_failures do it 'returns a JSON version of the account', :aggregate_failures do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
expect(response.media_type).to eq 'application/activity+json' .and have_attributes(
media_type: eq('application/activity+json')
)
expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end end
@ -188,12 +192,13 @@ RSpec.describe AccountsController do
let(:authorized_fetch_mode) { true } let(:authorized_fetch_mode) { true }
it 'returns a private signature JSON version of the account', :aggregate_failures do it 'returns a private signature JSON version of the account', :aggregate_failures do
expect(response).to have_http_status(200) expect(response)
.to have_http_status(200)
expect(response.media_type).to eq 'application/activity+json' .and have_attributes(
media_type: eq('application/activity+json')
)
expect(response.headers['Cache-Control']).to include 'private' expect(response.headers['Cache-Control']).to include 'private'
expect(response.headers['Vary']).to include 'Signature' expect(response.headers['Vary']).to include 'Signature'
expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
@ -207,60 +212,58 @@ RSpec.describe AccountsController do
context 'with a normal account in an RSS request' do context 'with a normal account in an RSS request' do
before do before do
get :show, params: { username: account.username, format: format } get short_account_path(username: account.username, format: format)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses', :aggregate_failures do it 'responds with correct statuses', :aggregate_failures do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.body).to include_status_tag(status_media) expect(response.body).to include(status_tag_for(status_media))
expect(response.body).to include_status_tag(status_self_reply) expect(response.body).to include(status_tag_for(status_self_reply))
expect(response.body).to include_status_tag(status) expect(response.body).to include(status_tag_for(status))
expect(response.body).to_not include_status_tag(status_direct) expect(response.body).to_not include(status_tag_for(status_direct))
expect(response.body).to_not include_status_tag(status_private) expect(response.body).to_not include(status_tag_for(status_private))
expect(response.body).to_not include_status_tag(status_reblog.reblog) expect(response.body).to_not include(status_tag_for(status_reblog.reblog))
expect(response.body).to_not include_status_tag(status_reply) expect(response.body).to_not include(status_tag_for(status_reply))
end end
end end
context 'with replies' do context 'with replies' do
before do before do
allow(controller).to receive(:replies_requested?).and_return(true) get short_account_with_replies_path(username: account.username, format: format)
get :show, params: { username: account.username, format: format }
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with replies', :aggregate_failures do it 'responds with correct statuses with replies', :aggregate_failures do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.body).to include_status_tag(status_media) expect(response.body).to include(status_tag_for(status_media))
expect(response.body).to include_status_tag(status_reply) expect(response.body).to include(status_tag_for(status_reply))
expect(response.body).to include_status_tag(status_self_reply) expect(response.body).to include(status_tag_for(status_self_reply))
expect(response.body).to include_status_tag(status) expect(response.body).to include(status_tag_for(status))
expect(response.body).to_not include_status_tag(status_direct) expect(response.body).to_not include(status_tag_for(status_direct))
expect(response.body).to_not include_status_tag(status_private) expect(response.body).to_not include(status_tag_for(status_private))
expect(response.body).to_not include_status_tag(status_reblog.reblog) expect(response.body).to_not include(status_tag_for(status_reblog.reblog))
end end
end end
context 'with media' do context 'with media' do
before do before do
allow(controller).to receive(:media_requested?).and_return(true) get short_account_media_path(username: account.username, format: format)
get :show, params: { username: account.username, format: format }
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with media', :aggregate_failures do it 'responds with correct statuses with media', :aggregate_failures do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.body).to include_status_tag(status_media) expect(response.body).to include(status_tag_for(status_media))
expect(response.body).to_not include_status_tag(status_direct) expect(response.body).to_not include(status_tag_for(status_direct))
expect(response.body).to_not include_status_tag(status_private) expect(response.body).to_not include(status_tag_for(status_private))
expect(response.body).to_not include_status_tag(status_reblog.reblog) expect(response.body).to_not include(status_tag_for(status_reblog.reblog))
expect(response.body).to_not include_status_tag(status_reply) expect(response.body).to_not include(status_tag_for(status_reply))
expect(response.body).to_not include_status_tag(status_self_reply) expect(response.body).to_not include(status_tag_for(status_self_reply))
expect(response.body).to_not include_status_tag(status) expect(response.body).to_not include(status_tag_for(status))
end end
end end
@ -270,30 +273,29 @@ RSpec.describe AccountsController do
let!(:status_tag) { Fabricate(:status, account: account) } let!(:status_tag) { Fabricate(:status, account: account) }
before do before do
allow(controller).to receive(:tag_requested?).and_return(true)
status_tag.tags << tag status_tag.tags << tag
get :show, params: { username: account.username, format: format, tag: tag.to_param } get short_account_tag_path(username: account.username, tag: tag, format: format)
end end
it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
it 'responds with correct statuses with a tag', :aggregate_failures do it 'responds with correct statuses with a tag', :aggregate_failures do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.body).to include_status_tag(status_tag) expect(response.body).to include(status_tag_for(status_tag))
expect(response.body).to_not include_status_tag(status_direct) expect(response.body).to_not include(status_tag_for(status_direct))
expect(response.body).to_not include_status_tag(status_media) expect(response.body).to_not include(status_tag_for(status_media))
expect(response.body).to_not include_status_tag(status_private) expect(response.body).to_not include(status_tag_for(status_private))
expect(response.body).to_not include_status_tag(status_reblog.reblog) expect(response.body).to_not include(status_tag_for(status_reblog.reblog))
expect(response.body).to_not include_status_tag(status_reply) expect(response.body).to_not include(status_tag_for(status_reply))
expect(response.body).to_not include_status_tag(status_self_reply) expect(response.body).to_not include(status_tag_for(status_self_reply))
expect(response.body).to_not include_status_tag(status) expect(response.body).to_not include(status_tag_for(status))
end end
end end
end end
end end
end end
def include_status_tag(status) def status_tag_for(status)
include ActivityPub::TagManager.instance.url_for(status) ActivityPub::TagManager.instance.url_for(status)
end end
end end