discourse/spec/requests/application_controller_spec.rb
Dan Ungureanu fdb1d3404c
FEATURE: Add site setting to show more detailed 404 errors. (#8014)
If the setting is turned on, then the user will receive information
about the subject: if it was deleted or requires some special access to
a group (only if the group is public). Otherwise, the user will receive
a generic #404 error message. For now, this change affects only the
topics and categories controller.

This commit also tries to refactor some of the code related to error
handling. To make error pages more consistent (design-wise), the actual
error page will be rendered server-side.
2019-10-08 14:15:08 +03:00

484 lines
15 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ApplicationController do
describe '#redirect_to_login_if_required' do
let(:admin) { Fabricate(:admin) }
before do
admin # to skip welcome wizard at home page `/`
SiteSetting.login_required = true
end
it "should never cache a login redirect" do
get "/"
expect(response.headers["Cache-Control"]).to eq("no-cache, no-store")
end
end
describe '#redirect_to_second_factor_if_required' do
let(:admin) { Fabricate(:admin) }
fab!(:user) { Fabricate(:user) }
before do
admin # to skip welcome wizard at home page `/`
end
it "should redirect admins when enforce_second_factor is 'all'" do
SiteSetting.enforce_second_factor = "all"
sign_in(admin)
get "/"
expect(response).to redirect_to("/u/#{admin.username}/preferences/second-factor")
end
it "should redirect users when enforce_second_factor is 'all'" do
SiteSetting.enforce_second_factor = "all"
sign_in(user)
get "/"
expect(response).to redirect_to("/u/#{user.username}/preferences/second-factor")
end
it "should not redirect anonymous users when enforce_second_factor is 'all'" do
SiteSetting.enforce_second_factor = "all"
SiteSetting.allow_anonymous_posting = true
sign_in(user)
post "/u/toggle-anon.json"
expect(response.status).to eq(200)
get "/"
expect(response.status).to eq(200)
end
it "should redirect admins when enforce_second_factor is 'staff'" do
SiteSetting.enforce_second_factor = "staff"
sign_in(admin)
get "/"
expect(response).to redirect_to("/u/#{admin.username}/preferences/second-factor")
end
it "should not redirect users when enforce_second_factor is 'staff'" do
SiteSetting.enforce_second_factor = "staff"
sign_in(user)
get "/"
expect(response.status).to eq(200)
end
it "should not redirect admins when turned off" do
SiteSetting.enforce_second_factor = "no"
sign_in(admin)
get "/"
expect(response.status).to eq(200)
end
it "should not redirect users when turned off" do
SiteSetting.enforce_second_factor = "no"
sign_in(user)
get "/"
expect(response.status).to eq(200)
end
end
describe 'invalid request params' do
before do
@old_logger = Rails.logger
@logs = StringIO.new
Rails.logger = Logger.new(@logs)
end
after do
Rails.logger = @old_logger
end
it 'should not raise a 500 (nor should it log a warning) for bad params' do
bad_str = (+"d\xDE").force_encoding('utf-8')
expect(bad_str.valid_encoding?).to eq(false)
get "/latest.json", params: { test: bad_str }
expect(response.status).to eq(400)
log = @logs.string
if (log.include? 'exception app middleware')
# heisentest diagnostics
puts
puts "EXTRA DIAGNOSTICS FOR INTERMITENT TEST FAIL"
puts log
puts ">> action_dispatch.exception"
ex = request.env['action_dispatch.exception']
puts ">> exception class: #{ex.class} : #{ex}"
end
expect(log).not_to include('exception app middleware')
expect(JSON.parse(response.body)).to eq(
"status" => 400,
"error" => "Bad Request"
)
end
end
describe 'missing required param' do
it 'should return a 400' do
get "/search/query.json", params: { trem: "misspelled term" }
expect(response.status).to eq(400)
expect(JSON.parse(response.body)).to eq(
"errors" => ["param is missing or the value is empty: term"]
)
end
end
describe 'build_not_found_page' do
describe 'topic not found' do
it 'should not redirect to permalink if topic/category does not exist' do
topic = create_post.topic
Permalink.create!(url: topic.relative_url, topic_id: topic.id + 1)
topic.trash!
SiteSetting.detailed_404 = false
get topic.relative_url
expect(response.status).to eq(404)
SiteSetting.detailed_404 = true
get topic.relative_url
expect(response.status).to eq(410)
end
it 'should return permalink for deleted topics' do
topic = create_post.topic
external_url = 'https://somewhere.over.rainbow'
Permalink.create!(url: topic.relative_url, external_url: external_url)
topic.trash!
get topic.relative_url
expect(response.status).to eq(301)
expect(response).to redirect_to(external_url)
get "/t/#{topic.id}.json"
expect(response.status).to eq(301)
expect(response).to redirect_to(external_url)
get "/t/#{topic.id}.json", xhr: true
expect(response.status).to eq(200)
expect(response.body).to eq(external_url)
end
it 'supports subfolder with permalinks' do
GlobalSetting.stubs(:relative_url_root).returns('/forum')
Discourse.stubs(:base_uri).returns("/forum")
trashed_topic = create_post.topic
trashed_topic.trash!
new_topic = create_post.topic
permalink = Permalink.create!(url: trashed_topic.relative_url, topic_id: new_topic.id)
# no subfolder because router doesn't know about subfolder in this test
get "/t/#{trashed_topic.slug}/#{trashed_topic.id}"
expect(response.status).to eq(301)
expect(response).to redirect_to("/forum/t/#{new_topic.slug}/#{new_topic.id}")
permalink.destroy
category = Fabricate(:category)
permalink = Permalink.create!(url: trashed_topic.relative_url, category_id: category.id)
get "/t/#{trashed_topic.slug}/#{trashed_topic.id}"
expect(response.status).to eq(301)
expect(response).to redirect_to("/forum/c/#{category.slug}")
permalink.destroy
permalink = Permalink.create!(url: trashed_topic.relative_url, post_id: new_topic.posts.last.id)
get "/t/#{trashed_topic.slug}/#{trashed_topic.id}"
expect(response.status).to eq(301)
expect(response).to redirect_to("/forum/t/#{new_topic.slug}/#{new_topic.id}/#{new_topic.posts.last.post_number}")
end
it 'should return 404 and show Google search for an invalid topic route' do
get "/t/nope-nope/99999999"
expect(response.status).to eq(404)
response_body = response.body
expect(response_body).to include(I18n.t('page_not_found.search_button'))
expect(response_body).to have_tag("input", with: { value: 'nope nope' })
end
it 'should not include Google search if login_required is enabled' do
SiteSetting.login_required = true
sign_in(Fabricate(:user))
get "/t/nope-nope/99999999"
expect(response.status).to eq(404)
expect(response.body).to_not include('google.com/search')
end
describe 'no logspam' do
before do
@orig_logger = Rails.logger
Rails.logger = @fake_logger = FakeLogger.new
end
after do
Rails.logger = @orig_logger
end
it 'should handle 404 to a css file' do
$redis.del("page_not_found_topics")
topic1 = Fabricate(:topic)
get '/stylesheets/mobile_1_4cd559272273fe6d3c7db620c617d596a5fdf240.css', headers: { 'HTTP_ACCEPT' => 'text/css,*/*,q=0.1' }
expect(response.status).to eq(404)
expect(response.body).to include(topic1.title)
topic2 = Fabricate(:topic)
get '/stylesheets/mobile_1_4cd559272273fe6d3c7db620c617d596a5fdf240.css', headers: { 'HTTP_ACCEPT' => 'text/css,*/*,q=0.1' }
expect(response.status).to eq(404)
expect(response.body).to include(topic1.title)
expect(response.body).to_not include(topic2.title)
expect(Rails.logger.fatals.length).to eq(0)
expect(Rails.logger.errors.length).to eq(0)
expect(Rails.logger.warnings.length).to eq(0)
end
end
it 'should cache results' do
$redis.del("page_not_found_topics")
topic1 = Fabricate(:topic)
get '/t/nope-nope/99999999'
expect(response.status).to eq(404)
expect(response.body).to include(topic1.title)
topic2 = Fabricate(:topic)
get '/t/nope-nope/99999999'
expect(response.status).to eq(404)
expect(response.body).to include(topic1.title)
expect(response.body).to_not include(topic2.title)
end
end
end
describe "#handle_theme" do
let!(:theme) { Fabricate(:theme, user_selectable: true) }
let!(:theme2) { Fabricate(:theme, user_selectable: true) }
let!(:non_selectable_theme) { Fabricate(:theme, user_selectable: false) }
fab!(:user) { Fabricate(:user) }
fab!(:admin) { Fabricate(:admin) }
before do
sign_in(user)
end
it "selects the theme the user has selected" do
user.user_option.update_columns(theme_ids: [theme.id])
get "/"
expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([theme.id])
theme.update_attribute(:user_selectable, false)
get "/"
expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([SiteSetting.default_theme_id])
end
it "can be overridden with a cookie" do
user.user_option.update_columns(theme_ids: [theme.id])
cookies['theme_ids'] = "#{theme2.id}|#{user.user_option.theme_key_seq}"
get "/"
expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([theme2.id])
theme2.update!(user_selectable: false, component: true)
theme.add_child_theme!(theme2)
cookies['theme_ids'] = "#{theme.id},#{theme2.id}|#{user.user_option.theme_key_seq}"
get "/"
expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([theme.id, theme2.id])
end
it "falls back to the default theme when the user has no cookies or preferences" do
user.user_option.update_columns(theme_ids: [])
cookies["theme_ids"] = nil
theme2.set_default!
get "/"
expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([theme2.id])
end
it "can be overridden with preview_theme_id param" do
sign_in(admin)
cookies['theme_ids'] = "#{theme.id},#{theme2.id}|#{admin.user_option.theme_key_seq}"
get "/", params: { preview_theme_id: theme2.id }
expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([theme2.id])
get "/", params: { preview_theme_id: non_selectable_theme.id }
expect(controller.theme_ids).to eq([non_selectable_theme.id])
end
it "does not allow non privileged user to preview themes" do
sign_in(user)
get "/", params: { preview_theme_id: non_selectable_theme.id }
expect(controller.theme_ids).to eq([SiteSetting.default_theme_id])
end
it "cookie can fail back to user if out of sync" do
user.user_option.update_columns(theme_ids: [theme.id])
cookies['theme_ids'] = "#{theme2.id}|#{user.user_option.theme_key_seq - 1}"
get "/"
expect(response.status).to eq(200)
expect(controller.theme_ids).to eq([theme.id])
end
end
describe 'Custom hostname' do
it 'does not allow arbitrary host injection' do
get("/latest",
headers: {
"X-Forwarded-Host" => "test123.com"
}
)
expect(response.body).not_to include("test123")
end
end
describe 'Delegated auth' do
let :public_key do
<<~TXT
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDh7BS7Ey8hfbNhlNAW/47pqT7w
IhBz3UyBYzin8JurEQ2pY9jWWlY8CH147KyIZf1fpcsi7ZNxGHeDhVsbtUKZxnFV
p16Op3CHLJnnJKKBMNdXMy0yDfCAHZtqxeBOTcCo1Vt/bHpIgiK5kmaekyXIaD0n
w0z/BYpOgZ8QwnI5ZwIDAQAB
-----END PUBLIC KEY-----
TXT
end
let :args do
{
auth_redirect: 'http://no-good.com',
user_api_public_key: "not-a-valid-public-key"
}
end
it 'disallows invalid public_key param' do
args[:auth_redirect] = "discourse://auth_redirect"
get "/latest", params: args
expect(response.body).to eq(I18n.t("user_api_key.invalid_public_key"))
end
it 'does not allow invalid auth_redirect' do
args[:user_api_public_key] = public_key
get "/latest", params: args
expect(response.body).to eq(I18n.t("user_api_key.invalid_auth_redirect"))
end
it 'does not redirect if one_time_password scope is disallowed' do
SiteSetting.allow_user_api_key_scopes = "read|write"
args[:user_api_public_key] = public_key
args[:auth_redirect] = "discourse://auth_redirect"
get "/latest", params: args
expect(response.status).to_not eq(302)
expect(response).to_not redirect_to("#{args[:auth_redirect]}?otp=true")
end
it 'redirects correctly with valid params' do
SiteSetting.login_required = true
args[:user_api_public_key] = public_key
args[:auth_redirect] = "discourse://auth_redirect"
get "/categories", params: args
expect(response.status).to eq(302)
expect(response).to redirect_to("#{args[:auth_redirect]}?otp=true")
end
end
describe 'Content Security Policy' do
it 'is enabled by SiteSettings' do
SiteSetting.content_security_policy = false
SiteSetting.content_security_policy_report_only = false
get '/'
expect(response.headers).to_not include('Content-Security-Policy')
expect(response.headers).to_not include('Content-Security-Policy-Report-Only')
SiteSetting.content_security_policy = true
SiteSetting.content_security_policy_report_only = true
get '/'
expect(response.headers).to include('Content-Security-Policy')
expect(response.headers).to include('Content-Security-Policy-Report-Only')
end
it 'can be customized with SiteSetting' do
SiteSetting.content_security_policy = true
get '/'
script_src = parse(response.headers['Content-Security-Policy'])['script-src']
expect(script_src).to_not include('example.com')
SiteSetting.content_security_policy_script_src = 'example.com'
get '/'
script_src = parse(response.headers['Content-Security-Policy'])['script-src']
expect(script_src).to include('example.com')
expect(script_src).to include("'unsafe-eval'")
end
it 'does not set CSP when responding to non-HTML' do
SiteSetting.content_security_policy = true
SiteSetting.content_security_policy_report_only = true
get '/latest.json'
expect(response.headers).to_not include('Content-Security-Policy')
expect(response.headers).to_not include('Content-Security-Policy-Report-Only')
end
def parse(csp_string)
csp_string.split(';').map do |policy|
directive, *sources = policy.split
[directive, sources]
end.to_h
end
end
it 'can respond to a request with */* accept header' do
get '/', headers: { HTTP_ACCEPT: '*/*' }
expect(response.status).to eq(200)
expect(response.body).to include('Discourse')
end
end