mirror of
https://github.com/discourse/discourse.git
synced 2024-11-24 00:51:03 +08:00
fdb1d3404c
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.
484 lines
15 KiB
Ruby
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
|