# 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 it "should redirect to login normally" do get "/" expect(response).to redirect_to("/login") end it "should redirect to SSO if enabled" do SiteSetting.discourse_connect_url = 'http://someurl.com' SiteSetting.enable_discourse_connect = true get "/" expect(response).to redirect_to("/session/sso") end it "should redirect to authenticator if only one, and local logins disabled" do # Local logins and google enabled, direct to login UI SiteSetting.enable_google_oauth2_logins = true get "/" expect(response).to redirect_to("/login") # Only google enabled, login immediately SiteSetting.enable_local_logins = false get "/" expect(response).to redirect_to("/auth/google_oauth2") # Google and GitHub enabled, direct to login UI SiteSetting.enable_github_logins = true get "/" expect(response).to redirect_to("/login") end it "should not redirect to SSO when auth_immediately is disabled" do SiteSetting.auth_immediately = false SiteSetting.discourse_connect_url = 'http://someurl.com' SiteSetting.enable_discourse_connect = true get "/" expect(response).to redirect_to("/login") end it "should not redirect to authenticator when auth_immediately is disabled" do SiteSetting.auth_immediately = false SiteSetting.enable_google_oauth2_logins = true SiteSetting.enable_local_logins = false get "/" expect(response).to redirect_to("/login") end context "with omniauth in test mode" do before do OmniAuth.config.test_mode = true OmniAuth.config.add_mock(:google_oauth2, info: OmniAuth::AuthHash::InfoHash.new( email: "address@example.com", ), extra: { raw_info: OmniAuth::AuthHash.new( email_verified: true, email: "address@example.com", ) } ) Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2] end after do Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2] = nil OmniAuth.config.test_mode = false end it "should not redirect to authenticator if registration in progress" do SiteSetting.enable_local_logins = false SiteSetting.enable_google_oauth2_logins = true get "/" expect(response).to redirect_to("/auth/google_oauth2") expect(cookies[:authentication_data]).to eq(nil) get "/auth/google_oauth2/callback.json" expect(response).to redirect_to("/") expect(cookies[:authentication_data]).not_to eq(nil) get "/" expect(response).to redirect_to("/login") end end it 'contains authentication data when cookies exist' do cookie_data = "someauthenticationdata" cookies['authentication_data'] = cookie_data get '/login' expect(response.status).to eq(200) expect(response.body).to include("data-authentication-data=\"#{cookie_data}\"") expect(response.headers["Set-Cookie"]).to include("authentication_data=;") # Delete cookie end it 'deletes authentication data cookie even if already authenticated' do sign_in(Fabricate(:user)) cookies['authentication_data'] = "someauthenticationdata" get '/' expect(response.status).to eq(200) expect(response.body).not_to include("data-authentication-data=") expect(response.headers["Set-Cookie"]).to include("authentication_data=;") # Delete cookie end it "returns a 403 for json requests" do get '/latest' expect(response.status).to eq(302) get '/latest.json' expect(response.status).to eq(403) 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 it "correctly redirects for Unicode usernames" do SiteSetting.enforce_second_factor = "all" SiteSetting.unicode_usernames = true user = sign_in(Fabricate(:unicode_user)) get "/" expect(response).to redirect_to("/u/#{user.encoded_username}/preferences/second-factor") end context "when enforcing second factor for staff" do before do SiteSetting.enforce_second_factor = "staff" sign_in(admin) end context "when the staff member has not enabled TOTP or security keys" do it "redirects the staff to the second factor preferences" do get "/" expect(response).to redirect_to("/u/#{admin.username}/preferences/second-factor") end end context "when the staff member has enabled TOTP" do before do Fabricate(:user_second_factor_totp, user: admin) end it "does not redirects the staff to set up 2FA" do get "/" expect(response.status).to eq(200) end end context "when the staff member has enabled security keys" do before do Fabricate(:user_security_key_with_random_credential, user: admin) end it "does not redirects the staff to set up 2FA" do get "/" expect(response.status).to eq(200) end end 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 INTERMITTENT 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(response.parsed_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(response.parsed_body["errors"].first).to include("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 set_subfolder "/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}/#{category.id}") 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 Discourse.cache.delete("page_not_found_topics:#{I18n.locale}") 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 Discourse.cache.delete("page_not_found_topics:#{I18n.locale}") Discourse.cache.delete("page_not_found_topics:fr") 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) # Different locale should have different cache SiteSetting.default_locale = :fr get '/t/nope-nope/99999999' expect(response.status).to eq(404) expect(response.body).to include(topic1.title) expect(response.body).to 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_id).to eq(theme.id) theme.update_attribute(:user_selectable, false) get "/" expect(response.status).to eq(200) expect(controller.theme_id).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_id).to eq(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_id).to eq(theme2.id) end it "can be overridden with preview_theme_id param" do sign_in(admin) cookies['theme_ids'] = "#{theme.id}|#{admin.user_option.theme_key_seq}" get "/", params: { preview_theme_id: theme2.id } expect(response.status).to eq(200) expect(controller.theme_id).to eq(theme2.id) get "/", params: { preview_theme_id: non_selectable_theme.id } expect(controller.theme_id).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_id).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_id).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 'allow_embedding_site_in_an_iframe' do it "should have the 'X-Frame-Options' header with value 'sameorigin'" do get("/latest") expect(response.headers['X-Frame-Options']).to eq("SAMEORIGIN") end it "should not include the 'X-Frame-Options' header" do SiteSetting.allow_embedding_site_in_an_iframe = true get("/latest") expect(response.headers).not_to include('X-Frame-Options') 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') 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 it 'when GTM is enabled it adds the same nonce to the policy and the GTM tag' do SiteSetting.content_security_policy = true SiteSetting.gtm_container_id = 'GTM-ABCDEF' get '/latest' nonce = ApplicationHelper.google_tag_manager_nonce expect(response.headers).to include('Content-Security-Policy') script_src = parse(response.headers['Content-Security-Policy'])['script-src'] expect(script_src.to_s).to include(nonce) expect(response.body).to include(nonce) 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 it 'has canonical tag' do get '/', headers: { HTTP_ACCEPT: '*/*' } expect(response.body).to have_tag("link", with: { rel: "canonical", href: "http://test.localhost/" }) get '/?query_param=true', headers: { HTTP_ACCEPT: '*/*' } expect(response.body).to have_tag("link", with: { rel: "canonical", href: "http://test.localhost/" }) get '/latest?page=2&additional_param=true', headers: { HTTP_ACCEPT: '*/*' } expect(response.body).to have_tag("link", with: { rel: "canonical", href: "http://test.localhost/latest?page=2" }) get '/404', headers: { HTTP_ACCEPT: '*/*' } expect(response.body).to have_tag("link", with: { rel: "canonical", href: "http://test.localhost/404" }) topic = create_post.topic get "/t/#{topic.slug}/#{topic.id}" expect(response.body).to have_tag("link", with: { rel: "canonical", href: "http://test.localhost/t/#{topic.slug}/#{topic.id}" }) end it "adds a noindex header if non-canonical indexing is disabled" do SiteSetting.allow_indexing_non_canonical_urls = false get '/' expect(response.headers['X-Robots-Tag']).to be_nil get '/latest' expect(response.headers['X-Robots-Tag']).to be_nil get '/categories' expect(response.headers['X-Robots-Tag']).to be_nil topic = create_post.topic get "/t/#{topic.slug}/#{topic.id}" expect(response.headers['X-Robots-Tag']).to be_nil post = create_post(topic_id: topic.id) get "/t/#{topic.slug}/#{topic.id}/2" expect(response.headers['X-Robots-Tag']).to eq('noindex') 20.times do create_post(topic_id: topic.id) end get "/t/#{topic.slug}/#{topic.id}/21" expect(response.headers['X-Robots-Tag']).to eq('noindex') get "/t/#{topic.slug}/#{topic.id}?page=2" expect(response.headers['X-Robots-Tag']).to be_nil end context "default locale" do before do SiteSetting.default_locale = :fr sign_in(Fabricate(:user)) end after do I18n.reload! end context "with rate limits" do before do RateLimiter.clear_all! RateLimiter.enable end it "serves a LimitExceeded error in the preferred locale" do SiteSetting.max_likes_per_day = 1 post1 = Fabricate(:post) post2 = Fabricate(:post) override = TranslationOverride.create( locale: "fr", translation_key: "rate_limiter.by_type.create_like", value: "French LimitExceeded error message" ) I18n.reload! post "/post_actions.json", params: { id: post1.id, post_action_type_id: PostActionType.types[:like] } expect(response.status).to eq(200) post "/post_actions.json", params: { id: post2.id, post_action_type_id: PostActionType.types[:like] } expect(response.status).to eq(429) expect(response.parsed_body["errors"].first).to eq(override.value) end end it "serves an InvalidParameters error with the default locale" do override = TranslationOverride.create( locale: "fr", translation_key: "invalid_params", value: "French InvalidParameters error message" ) I18n.reload! get "/search.json", params: { q: "hello\0hello" } expect(response.status).to eq(400) expect(response.parsed_body["errors"].first).to eq(override.value) end end describe "set_locale" do # Using /bootstrap.json because it returns a locale-dependent value def headers(locale) { HTTP_ACCEPT_LANGUAGE: locale } end context "allow_user_locale disabled" do context "accept-language header differs from default locale" do before do SiteSetting.allow_user_locale = false SiteSetting.default_locale = "en" end context "with an anonymous user" do it "uses the default locale" do get "/bootstrap.json", headers: headers("fr") expect(response.status).to eq(200) expect(response.parsed_body['bootstrap']['locale_script']).to end_with("en.js") end end context "with a logged in user" do it "it uses the default locale" do user = Fabricate(:user, locale: :fr) sign_in(user) get "/bootstrap.json", headers: headers("fr") expect(response.status).to eq(200) expect(response.parsed_body['bootstrap']['locale_script']).to end_with("en.js") end end end end context "set_locale_from_accept_language_header enabled" do context "accept-language header differs from default locale" do before do SiteSetting.allow_user_locale = true SiteSetting.set_locale_from_accept_language_header = true SiteSetting.default_locale = "en" end context "with an anonymous user" do it "uses the locale from the headers" do get "/bootstrap.json", headers: headers("fr") expect(response.status).to eq(200) expect(response.parsed_body['bootstrap']['locale_script']).to end_with("fr.js") end it "doesn't leak after requests" do get "/bootstrap.json", headers: headers("fr") expect(response.status).to eq(200) expect(response.parsed_body['bootstrap']['locale_script']).to end_with("fr.js") expect(I18n.locale.to_s).to eq(SiteSettings::DefaultsProvider::DEFAULT_LOCALE) end end context "with a logged in user" do let(:user) { Fabricate(:user, locale: :fr) } before do sign_in(user) end it "uses the user's preferred locale" do get "/bootstrap.json", headers: headers("fr") expect(response.status).to eq(200) expect(response.parsed_body['bootstrap']['locale_script']).to end_with("fr.js") end it "serves a 404 page in the preferred locale" do get "/missingroute", headers: headers("fr") expect(response.status).to eq(404) expected_title = I18n.t("page_not_found.title", locale: :fr) expect(response.body).to include(CGI.escapeHTML(expected_title)) end it "serves a RenderEmpty page in the preferred locale" do get "/u/#{user.username}/preferences/interface" expect(response.status).to eq(200) expect(response.body).to have_tag('script', with: { src: "/assets/locales/fr.js" }) end end end context "the preferred locale includes a region" do it "returns the locale and region separated by an underscore" do SiteSetting.allow_user_locale = true SiteSetting.set_locale_from_accept_language_header = true SiteSetting.default_locale = "en" get "/bootstrap.json", headers: headers("zh-CN") expect(response.status).to eq(200) expect(response.parsed_body['bootstrap']['locale_script']).to end_with("zh_CN.js") end end context 'accept-language header is not set' do it 'uses the site default locale' do SiteSetting.allow_user_locale = true SiteSetting.default_locale = 'en' get "/bootstrap.json", headers: headers("") expect(response.status).to eq(200) expect(response.parsed_body['bootstrap']['locale_script']).to end_with("en.js") end end end end describe 'vary header' do it 'includes Vary:Accept on all requests where format is not explicit' do # Rails default behaviour - include Vary:Accept when Accept is supplied get "/latest", headers: { "Accept" => "application/json" } expect(response.status).to eq(200) expect(response.headers["Vary"]).to eq("Accept") # Discourse additional behaviour (see lib/vary_header.rb) # Include Vary:Accept even when Accept is not supplied get "/latest" expect(response.status).to eq(200) expect(response.headers["Vary"]).to eq("Accept") # Not needed, because the path 'format' parameter overrides the Accept header get "/latest.json" expect(response.status).to eq(200) expect(response.headers["Vary"]).to eq(nil) end end describe "Discourse-Rate-Limit-Error-Code header" do fab!(:admin) { Fabricate(:admin) } before do RateLimiter.clear_all! RateLimiter.enable end it "is included when API key is rate limited" do global_setting :max_admin_api_reqs_per_minute, 1 api_key = ApiKey.create!(user_id: admin.id).key get "/latest.json", headers: { "Api-Key": api_key, "Api-Username": admin.username } expect(response.status).to eq(200) get "/latest.json", headers: { "Api-Key": api_key, "Api-Username": admin.username } expect(response.status).to eq(429) expect(response.headers["Discourse-Rate-Limit-Error-Code"]).to eq("admin_api_key_rate_limit") end it "is included when user API key is rate limited" do global_setting :max_user_api_reqs_per_minute, 1 user_api_key = UserApiKey.create!( user_id: admin.id, client_id: "", application_name: "discourseapp" ) user_api_key.scopes = UserApiKeyScope.all_scopes.keys.map do |name| UserApiKeyScope.create!(name: name, user_api_key_id: user_api_key.id) end user_api_key.save! get "/session/current.json", headers: { "User-Api-Key": user_api_key.key, } expect(response.status).to eq(200) get "/session/current.json", headers: { "User-Api-Key": user_api_key.key, } expect(response.status).to eq(429) expect( response.headers["Discourse-Rate-Limit-Error-Code"] ).to eq("user_api_key_limiter_60_secs") global_setting :max_user_api_reqs_per_minute, 100 global_setting :max_user_api_reqs_per_day, 1 get "/session/current.json", headers: { "User-Api-Key": user_api_key.key, } expect(response.status).to eq(429) expect( response.headers["Discourse-Rate-Limit-Error-Code"] ).to eq("user_api_key_limiter_1_day") end end describe "crawlers in slow_down_crawler_user_agents site setting" do before do RateLimiter.enable RateLimiter.clear_all! end it "are rate limited" do SiteSetting.slow_down_crawler_rate = 128 SiteSetting.slow_down_crawler_user_agents = "badcrawler|problematiccrawler" now = Time.zone.now freeze_time now get "/", headers: { "HTTP_USER_AGENT" => "iam badcrawler" } expect(response.status).to eq(200) get "/", headers: { "HTTP_USER_AGENT" => "iam badcrawler" } expect(response.status).to eq(429) expect(response.headers["Retry-After"]).to eq("128") get "/", headers: { "HTTP_USER_AGENT" => "iam problematiccrawler" } expect(response.status).to eq(200) get "/", headers: { "HTTP_USER_AGENT" => "iam problematiccrawler" } expect(response.status).to eq(429) expect(response.headers["Retry-After"]).to eq("128") freeze_time now + 100.seconds get "/", headers: { "HTTP_USER_AGENT" => "iam badcrawler" } expect(response.status).to eq(429) expect(response.headers["Retry-After"]).to eq("28") get "/", headers: { "HTTP_USER_AGENT" => "iam problematiccrawler" } expect(response.status).to eq(429) expect(response.headers["Retry-After"]).to eq("28") freeze_time now + 150.seconds get "/", headers: { "HTTP_USER_AGENT" => "iam badcrawler" } expect(response.status).to eq(200) get "/", headers: { "HTTP_USER_AGENT" => "iam problematiccrawler" } expect(response.status).to eq(200) end end end