discourse/spec/requests/search_controller_spec.rb
Jeff Wong de29b4a511
PERF: rate limit search, and add anon cache for search results (#9969)
Adds new hidden site settings for rate limits:
30 for logged in users, 15 for anon

Adds an anon cache for searching, caches results of searches for 1 minute
2020-06-04 09:26:08 -07:00

582 lines
16 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
describe SearchController do
fab!(:awesome_post) do
SearchIndexer.enable
Fabricate(:post, raw: 'this is my really awesome post')
end
fab!(:user) do
Fabricate(:user)
end
fab!(:user_post) do
SearchIndexer.enable
Fabricate(:post, raw: "#{user.username} is a cool person")
end
context "integration" do
before do
SearchIndexer.enable
end
before do
# TODO be a bit more strategic here instead of junking
# all of redis
Discourse.redis.flushdb
end
after do
Discourse.redis.flushdb
end
context "when overloaded" do
before do
global_setting :disable_search_queue_threshold, 0.2
end
let! :start_time do
freeze_time
Time.now
end
let! :current_time do
freeze_time 0.3.seconds.from_now
end
it "errors on #query" do
get "/search/query.json", headers: {
"HTTP_X_REQUEST_START" => "t=#{start_time.to_f}"
}, params: {
term: "hi there", include_blurb: true
}
expect(response.status).to eq(409)
end
it "no results and error on #index" do
get "/search.json", headers: {
"HTTP_X_REQUEST_START" => "t=#{start_time.to_f}"
}, params: {
q: "awesome"
}
expect(response.status).to eq(200)
data = response.parsed_body
expect(data["posts"]).to be_empty
expect(data["grouped_search_result"]["error"]).not_to be_empty
end
end
it "returns a 400 error if you search for null bytes" do
term = "hello\0hello"
get "/search/query.json", params: {
term: term, include_blurb: true
}
expect(response.status).to eq(400)
end
it "can search correctly" do
get "/search/query.json", params: {
term: 'awesome', include_blurb: true
}
expect(response.status).to eq(200)
data = response.parsed_body
expect(data['posts'].length).to eq(1)
expect(data['posts'][0]['id']).to eq(awesome_post.id)
expect(data['posts'][0]['blurb']).to eq(awesome_post.raw)
expect(data['topics'][0]['id']).to eq(awesome_post.topic_id)
end
it 'performs the query with a type filter' do
get "/search/query.json", params: {
term: user.username, type_filter: 'topic'
}
expect(response.status).to eq(200)
data = response.parsed_body
expect(data['posts'][0]['id']).to eq(user_post.id)
expect(data['users']).to be_blank
get "/search/query.json", params: {
term: user.username, type_filter: 'user'
}
expect(response.status).to eq(200)
data = response.parsed_body
expect(data['posts']).to be_blank
expect(data['users'][0]['id']).to eq(user.id)
end
context 'searching by topic id' do
it 'should not be restricted by minimum search term length' do
SiteSetting.min_search_term_length = 20000
get "/search/query.json", params: {
term: awesome_post.topic_id,
type_filter: 'topic',
search_for_id: true
}
expect(response.status).to eq(200)
data = response.parsed_body
expect(data['topics'][0]['id']).to eq(awesome_post.topic_id)
end
it "should return the right result" do
get "/search/query.json", params: {
term: user_post.topic_id,
type_filter: 'topic',
search_for_id: true
}
expect(response.status).to eq(200)
data = response.parsed_body
expect(data['topics'][0]['id']).to eq(user_post.topic_id)
end
end
end
context "#query" do
it "logs the search term" do
SiteSetting.log_search_queries = true
get "/search/query.json", params: { term: 'wookie' }
expect(response.status).to eq(200)
expect(SearchLog.where(term: 'wookie')).to be_present
json = response.parsed_body
search_log_id = json['grouped_search_result']['search_log_id']
expect(search_log_id).to be_present
log = SearchLog.where(id: search_log_id).first
expect(log).to be_present
expect(log.term).to eq('wookie')
end
it "doesn't log when disabled" do
SiteSetting.log_search_queries = false
get "/search/query.json", params: { term: 'wookie' }
expect(response.status).to eq(200)
expect(SearchLog.where(term: 'wookie')).to be_blank
end
context 'rate limited' do
before do
SiteSetting.rate_limit_search_user = 3
SiteSetting.rate_limit_search_anon = 2
end
it 'rate limits searches' do
RateLimiter.enable
RateLimiter.clear_all!
2.times do
get "/search/query.json", params: {
term: 'wookie'
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["grouped_search_result"]["error"]).to eq(nil)
end
get "/search/query.json", params: {
term: 'wookie'
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["grouped_search_result"]["error"]).to eq(I18n.t("rate_limiter.slow_down"))
end
context "and a logged in user" do
before { sign_in(user) }
it 'rate limits logged in searches' do
RateLimiter.enable
RateLimiter.clear_all!
3.times do
get "/search/query.json", params: {
term: 'wookie'
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["grouped_search_result"]["error"]).to eq(nil)
end
get "/search/query.json", params: {
term: 'wookie'
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["grouped_search_result"]["error"]).to eq(I18n.t("rate_limiter.slow_down"))
end
end
end
end
context "#show" do
it "doesn't raise an error when search term not specified" do
get "/search"
expect(response.status).to eq(200)
end
it "raises an error when the search term length is less than required" do
get "/search.json", params: { q: 'ba' }
expect(response.status).to eq(400)
end
it "raises an error when search term is a hash" do
get "/search.json?q[foo]"
expect(response.status).to eq(400)
end
it "returns a 400 error if you search for null bytes" do
term = "hello\0hello"
get "/search.json", params: { q: term }
expect(response.status).to eq(400)
end
it "logs the search term" do
SiteSetting.log_search_queries = true
get "/search.json", params: { q: 'bantha' }
expect(response.status).to eq(200)
expect(SearchLog.where(term: 'bantha')).to be_present
end
it "doesn't log when disabled" do
SiteSetting.log_search_queries = false
get "/search.json", params: { q: 'bantha' }
expect(response.status).to eq(200)
expect(SearchLog.where(term: 'bantha')).to be_blank
end
context 'rate limited' do
before do
SiteSetting.rate_limit_search_user = 3
SiteSetting.rate_limit_search_anon = 2
end
it 'rate limits searches' do
RateLimiter.enable
RateLimiter.clear_all!
2.times do
get "/search.json", params: {
q: 'bantha'
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["grouped_search_result"]["error"]).to eq(nil)
end
get "/search.json", params: {
q: 'bantha'
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["grouped_search_result"]["error"]).to eq(I18n.t("rate_limiter.slow_down"))
end
context "and a logged in user" do
before { sign_in(user) }
it 'rate limits searches' do
RateLimiter.enable
RateLimiter.clear_all!
3.times do
get "/search.json", params: {
q: 'bantha'
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["grouped_search_result"]["error"]).to eq(nil)
end
get "/search.json", params: {
q: 'bantha'
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["grouped_search_result"]["error"]).to eq(I18n.t("rate_limiter.slow_down"))
end
end
end
end
context "search priority" do
fab!(:low_priority_category) do
Fabricate(
:category,
search_priority: Searchable::PRIORITIES[:very_low]
)
end
fab!(:high_priority_category) do
Fabricate(
:category,
search_priority: Searchable::PRIORITIES[:high]
)
end
fab!(:very_high_priority_category) do
Fabricate(
:category,
search_priority: Searchable::PRIORITIES[:very_high]
)
end
fab!(:low_priority_topic) { Fabricate(:topic, category: low_priority_category) }
fab!(:high_priority_topic) { Fabricate(:topic, category: high_priority_category) }
fab!(:very_high_priority_topic) { Fabricate(:topic, category: very_high_priority_category) }
fab!(:low_priority_post) do
SearchIndexer.enable
Fabricate(:post, topic: low_priority_topic, raw: "This is a Low Priority Post")
end
fab!(:hight_priority_post) do
SearchIndexer.enable
Fabricate(:post, topic: high_priority_topic, raw: "This is a High Priority Post")
end
fab!(:old_very_hight_priority_post) do
SearchIndexer.enable
Fabricate(:old_post, topic: very_high_priority_topic, raw: "This is a Old but Very High Priority Post")
end
it "sort posts with search priority when search term is empty" do
get "/search.json", params: { q: 'status:open' }
expect(response.status).to eq(200)
data = response.parsed_body
post1 = data["posts"].find { |e| e["id"] == old_very_hight_priority_post.id }
post2 = data["posts"].find { |e| e["id"] == low_priority_post.id }
expect(data["posts"][0]["id"]).to eq(old_very_hight_priority_post.id)
expect(post1["id"]).to be > post2["id"]
end
it "sort posts with search priority when no order query" do
get "/search.json", params: { q: 'status:open Priority Post' }
expect(response.status).to eq(200)
data = response.parsed_body
expect(data["posts"][0]["id"]).to eq(old_very_hight_priority_post.id)
expect(data["posts"][1]["id"]).to eq(hight_priority_post.id)
expect(data["posts"][2]["id"]).to eq(low_priority_post.id)
end
it "doesn't sort posts with search piority when query with order" do
get "/search.json", params: { q: 'status:open order:latest Priority Post' }
expect(response.status).to eq(200)
data = response.parsed_body
expect(data["posts"][0]["id"]).to eq(hight_priority_post.id)
expect(data["posts"][1]["id"]).to eq(low_priority_post.id)
expect(data["posts"][2]["id"]).to eq(old_very_hight_priority_post.id)
end
end
context "search context" do
it "raises an error with an invalid context type" do
get "/search/query.json", params: {
term: 'test', search_context: { type: 'security', id: 'hole' }
}
expect(response.status).to eq(400)
end
it "raises an error with a missing id" do
get "/search/query.json",
params: { term: 'test', search_context: { type: 'user' } }
expect(response.status).to eq(400)
end
context "with a user" do
it "raises an error if the user can't see the context" do
get "/search/query.json", params: {
term: 'test', search_context: { type: 'private_messages', id: user.username }
}
expect(response).to be_forbidden
end
it 'performs the query with a search context' do
get "/search/query.json", params: {
term: 'test', search_context: { type: 'user', id: user.username }
}
expect(response.status).to eq(200)
end
end
context "with a tag" do
it "raises an error if the tag does not exist" do
get "/search/query.json", params: {
term: 'test', search_context: { type: 'tag', id: 'important-tag', name: 'important-tag' }
}
expect(response).to be_forbidden
end
it 'performs the query with a search context' do
Fabricate(:tag, name: 'important-tag')
get "/search/query.json", params: {
term: 'test', search_context: { type: 'tag', id: 'important-tag', name: 'important-tag' }
}
expect(response.status).to eq(200)
end
end
end
context "#click" do
after do
SearchLog.clear_debounce_cache!
end
it "doesn't work wthout the necessary parameters" do
post "/search/click.json"
expect(response.status).to eq(400)
end
it "doesn't record the click for a different user" do
sign_in(user)
_, search_log_id = SearchLog.log(
term: SecureRandom.hex,
search_type: :header,
user_id: -10,
ip_address: '127.0.0.1'
)
post "/search/click", params: {
search_log_id: search_log_id,
search_result_id: 12345,
search_result_type: 'topic'
}
expect(response.status).to eq(200)
expect(SearchLog.find(search_log_id).search_result_id).to be_blank
end
it "records the click for a logged in user" do
sign_in(user)
_, search_log_id = SearchLog.log(
term: SecureRandom.hex,
search_type: :header,
user_id: user.id,
ip_address: '127.0.0.1'
)
post "/search/click.json", params: {
search_log_id: search_log_id,
search_result_id: 12345,
search_result_type: 'user'
}
expect(response.status).to eq(200)
expect(SearchLog.find(search_log_id).search_result_id).to eq(12345)
expect(SearchLog.find(search_log_id).search_result_type).to eq(SearchLog.search_result_types[:user])
end
it "records the click for an anonymous user" do
get "/"
ip_address = request.remote_ip
_, search_log_id = SearchLog.log(
term: SecureRandom.hex,
search_type: :header,
ip_address: ip_address
)
post "/search/click.json", params: {
search_log_id: search_log_id,
search_result_id: 22222,
search_result_type: 'topic'
}
expect(response.status).to eq(200)
expect(SearchLog.find(search_log_id).search_result_id).to eq(22222)
expect(SearchLog.find(search_log_id).search_result_type).to eq(SearchLog.search_result_types[:topic])
end
it "doesn't record the click for a different IP" do
_, search_log_id = SearchLog.log(
term: SecureRandom.hex,
search_type: :header,
ip_address: '192.168.0.19'
)
post "/search/click", params: {
search_log_id: search_log_id,
search_result_id: 22222,
search_result_type: 'topic'
}
expect(response.status).to eq(200)
expect(SearchLog.find(search_log_id).search_result_id).to be_blank
end
it "records the click for search result type category" do
get "/"
ip_address = request.remote_ip
_, search_log_id = SearchLog.log(
term: SecureRandom.hex,
search_type: :header,
ip_address: ip_address
)
post "/search/click.json", params: {
search_log_id: search_log_id,
search_result_id: 23456,
search_result_type: 'category'
}
expect(response.status).to eq(200)
expect(SearchLog.find(search_log_id).search_result_id).to eq(23456)
expect(SearchLog.find(search_log_id).search_result_type).to eq(SearchLog.search_result_types[:category])
end
it "records the click for search result type tag" do
get "/"
ip_address = request.remote_ip
tag = Fabricate(:tag, name: 'test')
_, search_log_id = SearchLog.log(
term: SecureRandom.hex,
search_type: :header,
ip_address: ip_address
)
post "/search/click.json", params: {
search_log_id: search_log_id,
search_result_id: tag.name,
search_result_type: 'tag'
}
expect(response.status).to eq(200)
expect(SearchLog.find(search_log_id).search_result_id).to eq(tag.id)
expect(SearchLog.find(search_log_id).search_result_type).to eq(SearchLog.search_result_types[:tag])
end
end
end