# frozen_string_literal: true

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(@fake_logger.fatals.length).to eq(0)
          expect(@fake_logger.errors.length).to eq(0)
          expect(@fake_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 "splash_screen" do
    let(:admin) { Fabricate(:admin) }

    before do
      admin
    end

    it 'adds a preloader splash screen when enabled' do
      get '/'

      expect(response.status).to eq(200)
      expect(response.body).to include("d-splash")

      SiteSetting.splash_screen = false

      get '/'

      expect(response.status).to eq(200)
      expect(response.body).not_to include("d-splash")
    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

    it 'when splash screen is enabled it adds the same nonce to the policy and the inline splash script' do
      SiteSetting.content_security_policy = true
      SiteSetting.splash_screen = true

      get '/latest'
      nonce = ApplicationHelper.splash_screen_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 "with 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 "with allow_user_locale disabled" do
      context "when 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 "with set_locale_from_accept_language_header enabled" do
      context "when 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 "when 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 'when 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

  describe "#banner_json" do
    let(:admin) { Fabricate(:admin) }
    let(:user) { Fabricate(:user) }
    fab!(:banner_topic) { Fabricate(:banner_topic) }
    fab!(:p1) { Fabricate(:post, topic: banner_topic, raw: "A banner topic") }

    before do
      admin # to skip welcome wizard at home page `/`
    end

    context "with login_required" do
      before do
        SiteSetting.login_required = true
      end
      it "does not include banner info for anonymous users" do
        get "/login"

        expect(response.body).to have_tag("div#data-preloaded") do |element|
          json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
          expect(json['banner']).to eq("{}")
        end
      end

      it "includes banner info for logged-in users" do
        sign_in(user)
        get "/"

        expect(response.body).to have_tag("div#data-preloaded") do |element|
          json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
          expect(JSON.parse(json['banner'])["html"]).to eq("<p>A banner topic</p>")
        end
      end
    end

    context "with login not required" do
      before do
        SiteSetting.login_required = false
      end
      it "does include banner info for anonymous users" do
        get "/login"

        expect(response.body).to have_tag("div#data-preloaded") do |element|
          json = JSON.parse(element.current_scope.attribute('data-preloaded').value)
          expect(JSON.parse(json['banner'])["html"]).to eq("<p>A banner topic</p>")
        end
      end
    end
  end
end