# frozen_string_literal: true

RSpec.describe ApplicationController do
  fab!(:user)

  describe "handling PostgreSQL read-only errors" do
    after { Discourse.clear_postgres_readonly! }

    it "returns a read-only response" do
      get "/test_postgres_readonly.json"

      expect(response.status).to eq(503)
      expect(response.parsed_body).to eq(
        "errors" => [I18n.t("read_only_mode_enabled")],
        "error_type" => "read_only",
      )
    end
  end

  describe "shared session key" do
    before { SiteSetting.long_polling_base_url = "https://mb.example.com/" }

    it "renders the meta tag for a logged-in user" do
      sign_in(user)

      get "/latest"

      expect(response.body).to match(/<meta name="shared_session_key" content="[^"]+">/)
    end

    it "authenticates a login-required route via the header" do
      SiteSetting.login_required = true
      token = UserAuthToken.generate!(user_id: user.id)
      key = SecureRandom.hex
      Auth::DefaultCurrentUserProvider.store_shared_session_key(key, token.id.to_s)

      get "/latest.json"
      expect(response.status).to eq(403)

      get "/latest.json", headers: { "HTTP_X_SHARED_SESSION_KEY" => key }
      expect(response.status).to eq(200)
    end
  end

  describe "search metadata" do
    it "only advertises OpenSearch to users who can search" do
      SiteSetting.allow_anonymous_search = false

      get "/latest"
      expect(response.body).not_to have_tag("link[rel='search']")

      sign_in(user)
      get "/latest"
      expect(response.body).to have_tag("link[rel='search']")
    end
  end

  context "for cache control headers" do
    it "sets the `no-cache, no-store` cache control response header when no error is raised" do
      get "/latest"

      expect(response.status).to eq(200)
      expect(response.headers["Cache-Control"]).to eq("no-cache, no-store")
    end

    it "sets the `no-cache, no-store` cache control response header when ActionController::RoutingError is raised" do
      get "/invalid-urlllllllllll"

      expect(response.status).to eq(404)
      expect(response.headers["Cache-Control"]).to eq("no-cache, no-store")
    end

    it "sets the `no-cache, no-store` cache control response header when Discourse::InvalidAccess is raised" do
      get "/latest.json", headers: { HTTP_API_KEY: "invalid-api-key" }

      expect(response.status).to eq(403)
      expect(response.headers["Cache-Control"]).to eq("no-cache, no-store")
    end

    context "when cache_control_bfcache_compatibility is enabled" do
      before { SiteSetting.cache_control_bfcache_compatibility = true }

      it "sets bfcache-compatible cache control headers and includes the stale document reload script" do
        get "/latest"

        expect(response.status).to eq(200)
        expect(response.headers["Cache-Control"]).to eq("no-cache, private")
        expect(response.body).to include("bfcache-stale-document-check")
      end

      it "sets bfcache-compatible cache control headers on 404" do
        get "/invalid-urlllllllllll"

        expect(response.status).to eq(404)
        expect(response.headers["Cache-Control"]).to eq("no-cache, private")
      end

      it "sets bfcache-compatible cache control headers on 403" do
        get "/latest.json", headers: { HTTP_API_KEY: "invalid-api-key" }

        expect(response.status).to eq(403)
        expect(response.headers["Cache-Control"]).to eq("no-cache, private")
      end
    end

    it "does not include the stale document reload script in the HTML document by default" do
      get "/latest"

      expect(response.body).not_to include("bfcache-stale-document-check")
    end
  end

  context "when visiting an invalid URL" do
    it "displays the not found page with the user's active theme" do
      theme = Fabricate(:theme, user_selectable: true)
      Fabricate(:theme_field, theme:, name: "header", value: "<a>html</a>")
      user.user_option.update!(theme_ids: [theme.id])

      sign_in(user)

      get "/invalid-url"

      expect(response.status).to eq(404)
      expect(response.body).to include("<a>html</a>")
      expect(response.body).to include(I18n.t("page_not_found.title"))
    end
  end

  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 "does not cache login redirects" do
      get "/"
      expect(response.headers["Cache-Control"]).to eq("no-cache, no-store")
    end

    it "does not redirect to login" do
      get "/"
      expect(response).not_to redirect_to("/login")
      expect(response.status).to eq(200)
    end

    it "redirects to SSO when enabled" do
      SiteSetting.discourse_connect_url = "http://someurl.com"
      SiteSetting.discourse_connect_secret = "x" * 10
      SiteSetting.enable_discourse_connect = true
      get "/"
      expect(response).to redirect_to("/session/sso")
    end

    it "redirects to the sole authenticator when local logins are disabled" do
      # Local logins and google enabled, show login UI
      SiteSetting.enable_google_oauth2_logins = true
      get "/"
      expect(response).not_to redirect_to("/login")
      expect(response.status).to eq(200)

      # 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).not_to redirect_to("/login")
      expect(response.status).to eq(200)
    end

    it "does not redirect to SSO when auth_immediately is disabled" do
      SiteSetting.auth_immediately = false
      SiteSetting.discourse_connect_url = "http://someurl.com"
      SiteSetting.discourse_connect_secret = "x" * 10
      SiteSetting.enable_discourse_connect = true

      get "/"
      expect(response).not_to redirect_to("/login")
      expect(response.status).to eq(200)
    end

    it "does not redirect to the 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).not_to redirect_to("/login")
      expect(response.status).to eq(200)
    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 "does not redirect to the authenticator during registration" 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, refresh_auto_groups: true) }

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

    it "redirects admins when second factor is enforced for everyone" do
      SiteSetting.enforce_second_factor = "all"
      sign_in(admin)

      get "/"
      expect(response).to redirect_to("/u/#{admin.username}/preferences/second-factor")
    end

    it "includes the subfolder when redirecting admins for enforced second factor" do
      set_subfolder "/forum"
      SiteSetting.enforce_second_factor = "all"
      sign_in(admin)

      get "/"
      expect(response).to redirect_to("/forum/u/#{admin.username}/preferences/second-factor")
    end

    it "redirects users when second factor is enforced for everyone" do
      SiteSetting.enforce_second_factor = "all"
      sign_in(user)

      get "/"
      expect(response).to redirect_to("/u/#{user.username}/preferences/second-factor")
    end

    it "redirects OAuth users when second factor is enforced for everyone" do
      SiteSetting.enforce_second_factor = "all"
      sign_in(user)
      user.user_auth_tokens.last.update(authenticated_with_oauth: true)

      get "/"
      expect(response).to redirect_to("/u/#{user.username}/preferences/second-factor")
    end

    it "exempts OAuth users when second factor enforcement excludes external authentication" do
      SiteSetting.enforce_second_factor = "all"
      SiteSetting.enforce_second_factor_on_external_auth = false
      sign_in(user)
      user.user_auth_tokens.last.update(authenticated_with_oauth: true)

      get "/"
      expect(response.status).to eq(200)
    end

    it "does not redirect anonymous users for enforced second factor" do
      SiteSetting.enforce_second_factor = "all"
      SiteSetting.allow_anonymous_mode = true

      sign_in(user)

      post "/u/toggle-anon.json"
      expect(response.status).to eq(200)

      get "/"
      expect(response.status).to eq(200)
    end

    it "redirects admins when second factor is enforced for staff" do
      SiteSetting.enforce_second_factor = "staff"
      sign_in(admin)

      get "/"
      expect(response).to redirect_to("/u/#{admin.username}/preferences/second-factor")
    end

    it "does not redirect regular users when second factor is enforced for staff" do
      SiteSetting.enforce_second_factor = "staff"
      sign_in(user)

      get "/"
      expect(response.status).to eq(200)
    end

    it "does not redirect admins when enforcement is disabled" do
      SiteSetting.enforce_second_factor = "no"
      sign_in(admin)

      get "/"
      expect(response.status).to eq(200)
    end

    it "does not redirect users when enforcement is disabled" 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 { Fabricate(:user_second_factor_totp, user: admin) }

        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 { Fabricate(:user_security_key_with_random_credential, user: admin) }

        it "does not redirects the staff to set up 2FA" do
          get "/"
          expect(response.status).to eq(200)
        end
      end
    end
  end

  describe "#redirect_to_profile_if_required" do
    fab!(:user)

    before { sign_in(user) }

    context "when the user is missing required custom fields" do
      before do
        Fabricate(:user_field, requirement: "for_all_users")
        UserRequiredFieldsVersion.create!
      end

      it "redirects the user to the profile preferences" do
        get "/hot"
        expect(response).to redirect_to("/u/#{user.username}/preferences/profile")
      end

      it "only logs user history once per day" do
        expect do
          RateLimiter.enable
          get "/hot"
          get "/hot"
        end.to change { UserHistory.count }.by(1)
      end
    end

    context "when the user has filled up all required custom fields" do
      before do
        Fabricate(:user_field, requirement: "for_all_users")
        UserRequiredFieldsVersion.create!
        user.bump_required_fields_version
      end

      it "redirects the user to the profile preferences" do
        get "/hot"
        expect(response).not_to redirect_to("/u/#{user.username}/preferences/profile")
      end
    end
  end

  describe "invalid request params" do
    let(:fake_logger) { FakeLogger.new }

    before { Rails.logger.broadcast_to(fake_logger) }

    after { Rails.logger.stop_broadcasting_to(fake_logger) }

    it "handles invalid parameters without a 500 response or warning" 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)

      expect(fake_logger.warnings.length).to eq(0)
      expect(response.status).to eq(400)
    end
  end

  describe "missing required param" do
    it "returns 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 or invalid: term",
      )
    end
  end

  describe "build_not_found_page" do
    describe "topic not found" do
      it "does not redirect to a permalink for a missing topic or category" 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 "returns the 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 "returns 404 with 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 "omits Google search when login is required" 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

      it "does not include search when anonymous search is disabled" do
        SiteSetting.allow_anonymous_search = false

        get "/t/nope-nope/99999999"

        expect(response).to have_http_status(:not_found)
        expect(response.body).not_to include(I18n.t("page_not_found.search_title"))
      end

      it "allows anchor tags in the title" do
        TranslationOverride.upsert!(
          I18n.locale,
          "page_not_found.title",
          'Visit <a href="/search">search</a> page',
        )

        get "/t/nope-nope/99999999"
        expect(response.status).to eq(404)
        expect(response.body).to include('<a href="/search">search</a>')
      end

      it "sanitizes unsafe HTML in the title" do
        TranslationOverride.upsert!(
          I18n.locale,
          "page_not_found.title",
          'Page <script>alert("xss")</script> not found',
        )

        get "/t/nope-nope/99999999"
        expect(response.status).to eq(404)
        expect(response.body).to_not include("<script>")
        expect(response.body).to include("Page")
      end

      describe "no logspam" do
        let(:fake_logger) { FakeLogger.new }

        before { Rails.logger.broadcast_to(fake_logger) }

        after { Rails.logger.stop_broadcasting_to(fake_logger) }

        it "handles a missing 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

        it "renders category badge style classes on the 404 page" do
          Discourse.cache.delete("page_not_found_topics:#{I18n.locale}")

          square_cat = Fabricate(:category, style_type: :square)
          icon_cat = Fabricate(:category, style_type: :icon, icon: "user")
          emoji_cat = Fabricate(:category, style_type: :emoji, emoji: "smile")

          Fabricate(:topic, title: "Square Category Topic", category: square_cat)
          Fabricate(:topic, title: "Icon Category Topic", category: icon_cat)
          Fabricate(:topic, title: "Emoji Category Topic", category: emoji_cat)

          get "/t/nope-nope/99999999"
          expect(response.status).to eq(404)

          expect(response.body).to include("badge-category --style-square")
          expect(response.body).to include("badge-category --style-icon")
          expect(response.body).to include("badge-category --style-emoji")

          expect(response.body).to include('<svg id="user"')
          expect(response.body).to include('class="emoji"')
        end
      end

      it "does not retain topics moved to a restricted category" do
        Discourse.cache.delete("page_not_found_topics:#{I18n.locale}")
        topic = Fabricate(:topic_with_op, title: "restricted 404 cache topic")
        private_category = Fabricate(:private_category, group: Group[:staff])

        get "/missing-route"

        expect(response).to have_http_status(:not_found)
        expect(response.body).to include(topic.title)

        admin = sign_in(Fabricate(:admin))
        put "/t/#{topic.id}.json", params: { category_id: private_category.id }

        expect(response).to have_http_status(:ok)

        delete "/session/#{admin.username}.json"
        get "/missing-route"

        aggregate_failures do
          expect(response).to have_http_status(:not_found)
          expect(response.body).not_to include(topic.title)
        end
      end

      it "does not retain topics after a category becomes restricted" do
        Discourse.cache.delete("page_not_found_topics:#{I18n.locale}")
        category = Fabricate(:category)
        topic = Fabricate(:topic_with_op, title: "restricted category 404 cache topic", category:)

        get "/missing-route"

        expect(response).to have_http_status(:not_found)
        expect(response.body).to include(topic.title)

        admin = sign_in(Fabricate(:admin))
        put "/categories/#{category.id}.json",
            params: {
              permissions: {
                Group[:staff].name => CategoryGroup.permission_types[:full],
              },
            }

        expect(response).to have_http_status(:ok)

        delete "/session/#{admin.username}.json"
        get "/missing-route"

        aggregate_failures do
          expect(response).to have_http_status(:not_found)
          expect(response.body).not_to include(topic.title)
        end
      end

      it "caches 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)
    fab!(:admin)

    before { sign_in(user) }

    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 "sets X-Frame-Options to sameorigin" do
      get("/latest")
      expect(response.headers["X-Frame-Options"]).to eq("SAMEORIGIN")
    end

    it "omits 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 "setting `Cross-Origin-Opener-Policy` header" do
    describe "when `cross_origin_opener_policy_header` site setting is set to `same-origin`" do
      before { SiteSetting.cross_origin_opener_policy_header = "same-origin" }

      it "sets `Cross-Origin-Opener-Policy` header to `same-origin`" do
        get "/latest"

        expect(response.status).to eq(200)
        expect(response.headers["Cross-Origin-Opener-Policy"]).to eq("same-origin")
      end

      it "does not set the `Cross-Origin-Opener-Policy` header for a JSON request" do
        get "/latest.json"

        expect(response.status).to eq(200)
        expect(response.headers["Cross-Origin-Opener-Policy"]).to eq(nil)
      end
    end

    describe "when `cross_origin_opener_policy_header` site setting is set to `unsafe-none`" do
      it "does not set the `Cross-Origin-Opener-Policy` header" do
        SiteSetting.cross_origin_opener_policy_header = "unsafe-none"

        get "/latest"

        expect(response.status).to eq(200)
        expect(response.headers["Cross-Origin-Opener-Policy"]).to eq("unsafe-none")
      end
    end

    describe "when `cross_origin_opener_unsafe_none_groups` site setting has been set" do
      fab!(:group)
      fab!(:current_user, :user)

      before do
        SiteSetting.cross_origin_opener_policy_header = "same-origin"
        SiteSetting.cross_origin_opener_unsafe_none_groups = group.id
      end

      context "for logged in user" do
        before { sign_in(current_user) }

        it "sets `Cross-Origin-Opener-Policy` to `unsafe-none` for a listed group" do
          group.add(current_user)

          get "/latest"

          expect(response.status).to eq(200)
          expect(response.headers["Cross-Origin-Opener-Policy"]).to eq("unsafe-none")
        end

        it "sets `Cross-Origin-Opener-Policy` to configured value when group is missing" do
          get "/latest"

          expect(response.status).to eq(200)
          expect(response.headers["Cross-Origin-Opener-Policy"]).to eq("same-origin")
        end
      end

      context "for anon" do
        it "sets `Cross-Origin-Opener-Policy` to configured value" do
          get "/latest"

          expect(response.status).to eq(200)
          expect(response.headers["Cross-Origin-Opener-Policy"]).to eq("same-origin")
        end
      end
    end
  end

  describe "splash_screen" do
    let(:admin) { Fabricate(:admin) }

    before do
      admin
      allow_any_instance_of(ApplicationController).to receive(:include_splash_screen?).and_return(
        true,
      )
    end

    it "adds a preloader splash screen" do
      get "/"

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

    context "with color schemes" do
      let!(:light_scheme) do
        ColorScheme.find_by(base_scheme_id: ColorScheme::NAMES_TO_ID_MAP["Solarized Light"])
      end
      let!(:dark_scheme) do
        ColorScheme.find_by(base_scheme_id: ColorScheme::NAMES_TO_ID_MAP["Dark"])
      end

      before do
        SiteSetting.interface_color_selector = "sidebar_footer"
        Theme.find_default.update!(
          color_scheme_id: light_scheme.id,
          dark_color_scheme_id: dark_scheme.id,
        )
      end

      context "when light mode is forced" do
        before { cookies[:forced_color_mode] = "light" }

        it "uses the light scheme colors and doesn't include the prefers-color-scheme media query" do
          get "/"

          style = css_select("#d-splash style").to_s
          expect(style).not_to include("prefers-color-scheme")

          secondary = light_scheme.colors.find { |color| color.name == "secondary" }.hex
          tertiary = light_scheme.colors.find { |color| color.name == "tertiary" }.hex
          expect(style).to include(<<~CSS.indent(6))
            html {
              background-color: ##{secondary};
            }
          CSS
          expect(style).to include(<<~CSS.indent(6))
            #d-splash {
              --dot-color: ##{tertiary};
            }
          CSS
        end
      end

      context "when dark mode is forced" do
        before { cookies[:forced_color_mode] = "dark" }

        it "uses the dark scheme colors and doesn't include the prefers-color-scheme media query" do
          get "/"

          style = css_select("#d-splash style").to_s
          expect(style).not_to include("prefers-color-scheme")

          secondary = dark_scheme.colors.find { |color| color.name == "secondary" }.hex
          tertiary = dark_scheme.colors.find { |color| color.name == "tertiary" }.hex
          expect(style).to include(<<~CSS.indent(6))
            html {
              background-color: ##{secondary};
            }
          CSS
          expect(style).to include(<<~CSS.indent(6))
            #d-splash {
              --dot-color: ##{tertiary};
            }
          CSS
        end
      end

      context "when no color mode is forced" do
        before { cookies[:forced_color_mode] = nil }

        it "includes both dark and light colors inside prefers-color-scheme media queries" do
          get "/"

          style = css_select("#d-splash style").to_s

          light_secondary = light_scheme.colors.find { |color| color.name == "secondary" }.hex
          light_tertiary = light_scheme.colors.find { |color| color.name == "tertiary" }.hex

          dark_secondary = dark_scheme.colors.find { |color| color.name == "secondary" }.hex
          dark_tertiary = dark_scheme.colors.find { |color| color.name == "tertiary" }.hex

          expect(style).to include(<<~CSS.indent(6))
            @media (prefers-color-scheme: light) {
              html {
                background-color: ##{light_secondary};
              }

              #d-splash {
                --dot-color: ##{light_tertiary};
              }
            }
          CSS

          expect(style).to include(<<~CSS.indent(6))
            @media (prefers-color-scheme: dark) {
              html {
                background-color: ##{dark_secondary};
              }

              #d-splash {
                --dot-color: ##{dark_tertiary};
              }
            }
          CSS
        end
      end
    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

    it "does not allow auth_redirect that differs from a registered client's redirect" do
      SiteSetting.allowed_user_api_auth_redirects = "https://*.example.com/callback"

      Fabricate(
        :user_api_key_client,
        public_key: public_key,
        auth_redirect: "https://legitimate.example.com/callback",
      )

      args[:user_api_public_key] = public_key
      args[:auth_redirect] = "https://evil.example.com/callback"

      get "/latest", params: args

      expect(response.body).to eq(I18n.t("user_api_key.invalid_auth_redirect"))
    end

    it "redirects when auth_redirect matches a registered client's redirect and global allowlist" do
      SiteSetting.allowed_user_api_auth_redirects = "https://legitimate.example.com/callback"

      Fabricate(
        :user_api_key_client,
        public_key: public_key,
        auth_redirect: "https://legitimate.example.com/callback",
      )

      args[:user_api_public_key] = public_key
      args[:auth_redirect] = "https://legitimate.example.com/callback"

      get "/latest", params: args

      expect(response.status).to eq(302)
      expect(response).to redirect_to("#{args[:auth_redirect]}?otp=true")
    end

    it "falls back to global wildcards when client has no registered auth_redirect" do
      SiteSetting.allowed_user_api_auth_redirects = "discourse://auth_redirect"

      Fabricate(:user_api_key_client, public_key: public_key, auth_redirect: nil)

      args[:user_api_public_key] = public_key
      args[:auth_redirect] = "discourse://auth_redirect"

      get "/latest", 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("'unsafe-eval'")

      SiteSetting.content_security_policy_script_src = "'unsafe-eval'"

      get "/"
      script_src = parse(response.headers["Content-Security-Policy"])["script-src"]

      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

    it "when GTM is enabled it adds the same nonce to the policy and the GTM tag" do
      SiteSetting.content_security_policy = true
      SiteSetting.content_security_policy_report_only = true
      SiteSetting.gtm_container_id = "GTM-ABCDEF"

      get "/latest"

      script_src = parse(response.headers["Content-Security-Policy"])["script-src"]
      report_only_script_src =
        parse(response.headers["Content-Security-Policy-Report-Only"])["script-src"]

      nonce = extract_nonce_from_script_src(script_src)
      report_only_nonce = extract_nonce_from_script_src(report_only_script_src)

      expect(nonce).to eq(report_only_nonce)

      gtm_meta_tag = Nokogiri::HTML5.fragment(response.body).css("#data-google-tag-manager").first
      expect(gtm_meta_tag["data-nonce"]).to eq(nonce)
    end

    it "doesn't reuse nonces between requests" do
      global_setting :anon_cache_store_threshold, 1
      Middleware::AnonymousCache.enable_anon_cache
      Middleware::AnonymousCache.clear_all_cache!

      SiteSetting.content_security_policy = true
      SiteSetting.content_security_policy_report_only = true
      SiteSetting.gtm_container_id = "GTM-ABCDEF"

      get "/latest"

      expect(response.headers["X-Discourse-Cached"]).to eq("store")
      expect(response.headers).not_to include("Discourse-CSP-Nonce-Placeholder")

      script_src = parse(response.headers["Content-Security-Policy"])["script-src"]
      report_only_script_src =
        parse(response.headers["Content-Security-Policy-Report-Only"])["script-src"]

      first_nonce = extract_nonce_from_script_src(script_src)
      first_report_only_nonce = extract_nonce_from_script_src(report_only_script_src)

      expect(first_nonce).to eq(first_report_only_nonce)

      gtm_meta_tag = Nokogiri::HTML5.fragment(response.body).css("#data-google-tag-manager").first
      expect(gtm_meta_tag["data-nonce"]).to eq(first_nonce)

      get "/latest"

      expect(response.headers["X-Discourse-Cached"]).to eq("true")
      expect(response.headers).not_to include("Discourse-CSP-Nonce-Placeholder")

      script_src = parse(response.headers["Content-Security-Policy"])["script-src"]
      report_only_script_src =
        parse(response.headers["Content-Security-Policy-Report-Only"])["script-src"]

      second_nonce = extract_nonce_from_script_src(script_src)
      second_report_only_nonce = extract_nonce_from_script_src(report_only_script_src)

      expect(second_nonce).to eq(second_report_only_nonce)

      expect(first_nonce).not_to eq(second_nonce)
      gtm_meta_tag = Nokogiri::HTML5.fragment(response.body).css("#data-google-tag-manager").first
      expect(gtm_meta_tag["data-nonce"]).to eq(second_nonce)
    end

    def parse(csp_string)
      csp_string
        .split(";")
        .map do |policy|
          directive, *sources = policy.split
          [directive, sources]
        end
        .to_h
    end

    def extract_nonce_from_script_src(script_src)
      nonce = script_src.lazy.map { |src| src[/\A'nonce-([^']+)'\z/, 1] }.find(&:itself)
      expect(nonce).to be_present
      nonce
    end
  end

  describe "browser pageview tracking session id" do
    it "doesn't reuse session ids between requests served from the anon cache" do
      global_setting :anon_cache_store_threshold, 1
      Middleware::AnonymousCache.enable_anon_cache
      Middleware::AnonymousCache.clear_all_cache!

      SiteSetting.trigger_browser_pageview_events = true

      get "/latest"

      expect(response.headers["X-Discourse-Cached"]).to eq("store")
      expect(response.headers).not_to include(
        Middleware::TrackViewSessionIdInjector::PLACEHOLDER_HEADER,
      )

      session_id_format = /\A[A-Za-z0-9]{#{Middleware::RequestTracker::MAX_SESSION_ID_LENGTH}}\z/

      first_session_id = extract_session_id_from_body(response.body)
      expect(first_session_id).to match(session_id_format)

      get "/latest"

      expect(response.headers["X-Discourse-Cached"]).to eq("true")
      expect(response.headers).not_to include(
        Middleware::TrackViewSessionIdInjector::PLACEHOLDER_HEADER,
      )

      second_session_id = extract_session_id_from_body(response.body)
      expect(second_session_id).to match(session_id_format)

      expect(first_session_id).not_to eq(second_session_id)
    end

    def extract_session_id_from_body(body)
      meta_tag =
        Nokogiri::HTML5.fragment(body).css("meta[name='discourse-track-view-session-id']").first
      expect(meta_tag).to be_present
      meta_tag["content"]
    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 { create_post(topic_id: topic.id) }
    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 { I18n.reload! }

    context "with rate limits" do
      before { RateLimiter.enable }

      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

    def main_locale_scripts(body)
      Nokogiri::HTML5
        .parse(body)
        .css('script[src*="extra-locales/"]')
        .filter_map do |script|
          script.attributes["src"].to_s[%r{extra-locales/[^/]+/([^/]+)/main.js}, 1]
        end
    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 "/latest", headers: headers("fr")
            expect(response.status).to eq(200)
            expect(main_locale_scripts(response.body)).to contain_exactly("en")
          end
        end

        context "with a logged in user" do
          it "uses the default locale" do
            user = Fabricate(:user, locale: :fr)
            sign_in(user)

            get "/latest", headers: headers("fr")
            expect(response.status).to eq(200)
            expect(main_locale_scripts(response.body)).to contain_exactly("en")
          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 "/latest", headers: headers("fr")
            expect(response.status).to eq(200)
            expect(main_locale_scripts(response.body)).to contain_exactly("fr")
          end

          it "doesn't leak after requests" do
            get "/latest", headers: headers("fr")
            expect(response.status).to eq(200)
            expect(main_locale_scripts(response.body)).to contain_exactly("fr")
            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 { sign_in(user) }

          it "uses the user's preferred locale" do
            get "/latest", headers: headers("fr")
            expect(response.status).to eq(200)
            expect(main_locale_scripts(response.body)).to contain_exactly("fr")
          end

          it "serves a 404 page in the preferred locale" do
            get "/missingroute", headers: headers("fr")
            expect(response.status).to eq(404)
            expect(response.body).to include(
              # converts non-breaking space to &nbsp;
              ActionController::Base.helpers.sanitize(
                I18n.t("page_not_found.title", locale: :fr),
                tags: %w[a],
                attributes: %w[href class target rel],
              ),
            )
          end

          it "serves a RenderEmpty page in the preferred locale" do
            get "/u/#{user.username}/preferences/interface"
            expect(response.status).to eq(200)
            expect(main_locale_scripts(response.body)).to contain_exactly("fr")
          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 "/latest", headers: headers("zh-CN")
          expect(response.status).to eq(200)
          expect(main_locale_scripts(response.body)).to contain_exactly("zh_CN")
        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 "/latest", headers: headers("")
          expect(response.status).to eq(200)
          expect(main_locale_scripts(response.body)).to contain_exactly("en")
        end
      end
    end

    context "with a logged in user whose interface language differs from the default locale" do
      let(:user) { Fabricate(:user, locale: :ja) }

      before do
        SiteSetting.allow_user_locale = true
        SiteSetting.default_locale = "en"
        sign_in(user)
      end

      it "serves the whole not-found page, including the title, in the user's locale" do
        get "/missingroute"
        expect(response.status).to eq(404)

        # the body is rendered in the user's interface language...
        expect(response.body).to include(I18n.t("page_not_found.home", locale: :ja))
        expect(response.body).to include(I18n.t("page_not_found.search_title", locale: :ja))

        # ...and so is the <h1> title
        expect(response.body).to include(
          ActionController::Base.helpers.sanitize(
            I18n.t("page_not_found.title", locale: :ja),
            tags: %w[a],
            attributes: %w[href class target rel],
          ),
        )
      end

      it "serves the forbidden page title in the user's locale" do
        SiteSetting.detailed_404 = true
        private_category = Fabricate(:private_category, group: Fabricate(:group))

        get "/c/#{private_category.slug}/l/latest"
        expect(response.status).to eq(403)
        expect(response.body).to include(I18n.t("page_forbidden.title", locale: :ja))
      end

      it "serves the SPA-injected error panel (JSON extras) in the user's locale" do
        private_category = Fabricate(:private_category, group: Fabricate(:group))
        private_topic = Fabricate(:topic, category: private_category)

        get "/t/#{private_topic.slug}/#{private_topic.id}.json"
        expect(response.status).to eq(404)

        extras = response.parsed_body["extras"]
        expect(extras["title"]).to eq(I18n.t("page_not_found.page_title", locale: :ja))
        expect(extras["html"]).to include(I18n.t("page_not_found.title", locale: :ja))
      end
    end

    context "with set_locale_from_cookie enabled" do
      context "when cookie locale differs from default locale" do
        before do
          SiteSetting.allow_user_locale = true
          SiteSetting.set_locale_from_cookie = true
          SiteSetting.default_locale = "en"
        end

        context "with an anonymous user" do
          it "uses the locale from the cookie" do
            get "/latest", headers: { Cookie: "locale=es" }
            expect(response.status).to eq(200)
            expect(main_locale_scripts(response.body)).to contain_exactly("es")
            expect(I18n.locale.to_s).to eq(SiteSettings::DefaultsProvider::DEFAULT_LOCALE) # doesn't leak after requests
          end
        end

        context "when the preferred locale includes a region" do
          it "returns the locale and region separated by an underscore" do
            get "/latest", headers: { Cookie: "locale=zh-CN" }
            expect(response.status).to eq(200)
            expect(main_locale_scripts(response.body)).to contain_exactly("zh_CN")
          end
        end
      end

      context "when locale cookie is not set" do
        it "uses the site default locale" do
          SiteSetting.allow_user_locale = true
          SiteSetting.default_locale = "en"

          get "/latest", headers: { Cookie: "" }
          expect(response.status).to eq(200)
          expect(main_locale_scripts(response.body)).to contain_exactly("en")
        end
      end
    end

    context "with the language switcher enabled and set_locale_from_cookie disabled" do
      before do
        SiteSetting.allow_user_locale = true
        SiteSetting.default_locale = "en"
        SiteSetting.set_locale_from_cookie = false
        SiteSetting.content_localization_supported_locales = "es|fr"
        SiteSetting.content_localization_enabled = true
        SiteSetting.content_localization_language_switcher = "all"
      end

      context "with an anonymous user" do
        it "uses the locale from the cookie" do
          get "/latest", headers: { Cookie: "locale=es" }
          expect(response.status).to eq(200)
          expect(main_locale_scripts(response.body)).to contain_exactly("es")
        end

        it "ignores a locale the site has not configured" do
          get "/latest", headers: { Cookie: "locale=ja" }
          expect(response.status).to eq(200)
          expect(main_locale_scripts(response.body)).to contain_exactly("en")
        end

        it "ignores the cookie once the switcher is turned off" do
          SiteSetting.content_localization_language_switcher = "none"

          get "/latest", headers: { Cookie: "locale=es" }
          expect(response.status).to eq(200)
          expect(main_locale_scripts(response.body)).to contain_exactly("en")
        end
      end

      context "with a logged-in user" do
        fab!(:user) { Fabricate(:user, locale: "fr") }

        it "ignores the cookie and uses the user's preference" do
          sign_in(user)
          # Set through the jar rather than a Cookie header, which would drop the auth cookie.
          cookies[:locale] = "es"

          get "/latest"
          expect(response.status).to eq(200)
          expect(main_locale_scripts(response.body)).to contain_exactly("fr")
        end
      end
    end

    context "with set_locale_from_param" do
      context "when param locale differs from default locale" do
        before do
          SiteSetting.allow_user_locale = true
          SiteSetting.default_locale = "en"
        end

        context "with an anonymous user" do
          it "uses the locale from the param" do
            SiteSetting.set_locale_from_param = true

            get "/latest?tl=es"
            expect(response.status).to eq(200)
            expect(main_locale_scripts(response.body)).to contain_exactly("es")
            expect(I18n.locale.to_s).to eq(SiteSettings::DefaultsProvider::DEFAULT_LOCALE) # doesn't leak after requests
          end

          it "sets a cookie with the locale from the param for persistence" do
            SiteSetting.set_locale_from_param = false
            SiteSetting.set_locale_from_cookie = false
            get "/latest?tl=ja"
            expect(response.status).to eq(200)
            expect(response.cookies["locale"]).to eq(nil)

            SiteSetting.set_locale_from_param = true
            SiteSetting.set_locale_from_cookie = false
            get "/latest?tl=ja"
            expect(response.status).to eq(200)
            expect(response.cookies["locale"]).to eq(nil)

            SiteSetting.set_locale_from_param = true
            SiteSetting.set_locale_from_cookie = true
            get "/latest?tl=ja"
            expect(response.status).to eq(200)
            expect(response.cookies["locale"]).to eq("ja")
          end

          it "does not set a cookie for invalid locales" do
            SiteSetting.set_locale_from_param = true
            SiteSetting.set_locale_from_cookie = true

            get "/latest?tl=invalid_locale"
            expect(response.status).to eq(200)
            expect(response.cookies["locale"]).to be_nil
          end

          it "persists locale across requests via cookie" do
            SiteSetting.set_locale_from_param = true
            SiteSetting.set_locale_from_cookie = true

            get "/latest?tl=ja"
            expect(response.status).to eq(200)
            expect(response.cookies["locale"]).to eq("ja")
            expect(main_locale_scripts(response.body)).to contain_exactly("ja")

            # next request without tl parameter should use the cookie
            get "/latest", headers: { Cookie: "locale=ja" }
            expect(response.status).to eq(200)
            expect(main_locale_scripts(response.body)).to contain_exactly("ja")
          end
        end

        context "with a logged-in user" do
          fab!(:user) { Fabricate(:user, locale: "de") }

          it "ignores the tl parameter and uses user's preference" do
            sign_in(user)
            get "/latest?tl=es"
            expect(response.status).to eq(200)
            expect(main_locale_scripts(response.body)).to contain_exactly("de")
            expect(response.cookies["locale"]).to be_nil
          end
        end

        context "when the preferred locale includes a region" do
          it "returns the locale and region separated by an underscore" do
            SiteSetting.set_locale_from_param = true

            get "/latest?tl=zh-CN"
            expect(response.status).to eq(200)
            expect(main_locale_scripts(response.body)).to contain_exactly("zh_CN")
          end
        end
      end

      context "when locale param is not set" do
        it "uses the site default locale" do
          SiteSetting.allow_user_locale = true
          SiteSetting.default_locale = "en"

          get "/latest"
          expect(response.status).to eq(200)
          expect(main_locale_scripts(response.body)).to contain_exactly("en")
        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)

    before { RateLimiter.enable }

    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)
      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
      Fabricate(:admin) # to prevent redirect to the wizard
      RateLimiter.enable

      SiteSetting.slow_down_crawler_rate = 128
      SiteSetting.slow_down_crawler_user_agents = "badcrawler|problematiccrawler"
    end

    it "are rate limited" do
      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

    context "with anonymous caching" do
      before do
        global_setting :anon_cache_store_threshold, 1
        Middleware::AnonymousCache.enable_anon_cache
      end

      it "don't bypass crawler rate limits" do
        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)
      end
    end
  end

  describe "#banner_json" do
    let(:admin) { Fabricate(:admin) }
    let(:user) { Fabricate(:user) }

    fab!(: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 { SiteSetting.login_required = true }

      it "does not include banner info for anonymous users" do
        get "/login"

        expect(response.body).to have_tag("script#data-preloaded") do |element|
          json = JSON.parse(element.current_scope.text)
          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("script#data-preloaded") do |element|
          json = JSON.parse(element.current_scope.text)
          expect(JSON.parse(json["banner"])["html"]).to eq("<p>A banner topic</p>")
        end
      end
    end

    context "with login not required" do
      before { SiteSetting.login_required = false }

      it "does include banner info for anonymous users" do
        get "/login"

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

    context "with content localization enabled" do
      def banner_html
        preloaded = Nokogiri::HTML5.fragment(response.body).css("#data-preloaded").first
        JSON.parse(JSON.parse(preloaded.text)["banner"])["html"]
      end

      before do
        SiteSetting.allow_user_locale = true
        SiteSetting.set_locale_from_cookie = true
        SiteSetting.content_localization_enabled = true
        SiteSetting.login_required = false
        p1.update!(locale: "en")
        p1.topic.update!(locale: "en")
        Fabricate(:post_localization, post: p1, locale: "de", cooked: "<p>German banner</p>")
        Fabricate(:post_localization, post: p1, locale: "zh_CN", cooked: "<p>Chinese banner</p>")
        ApplicationLayoutPreloader.banner_json_cache.clear
      end

      it "caches banner separately per locale to prevent cache poisoning" do
        get "/login", headers: { Cookie: "locale=de" }
        expect(banner_html).to eq("<p>German banner</p>")

        get "/login", headers: { Cookie: "locale=zh_CN" }
        expect(banner_html).to eq("<p>Chinese banner</p>")

        get "/login"
        expect(banner_html).to eq("<p>A banner topic</p>")
      end
    end
  end

  describe "Early hint header" do
    before { global_setting :cdn_url, "https://cdn.example.com/something" }

    it "is not included by default" do
      get "/latest"
      expect(response.status).to eq(200)
      expect(response.headers["Link"]).to eq(nil)
    end

    context "when in preconnect mode" do
      before { global_setting :early_hint_header_mode, "preconnect" }

      it "includes the preconnect hint" do
        get "/latest"
        expect(response.status).to eq(200)
        expect(response.headers["Link"]).to include("<https://cdn.example.com>; rel=preconnect")
        expect(response.headers["Link"]).not_to include("rel=preload")
      end

      it "can use a different header" do
        global_setting :early_hint_header_name, "X-Discourse-Early-Hint"
        get "/latest"
        expect(response.status).to eq(200)
        expect(response.headers["X-Discourse-Early-Hint"]).to include(
          "<https://cdn.example.com>; rel=preconnect",
        )
        expect(response.headers["Link"]).to eq(nil)
      end

      it "is skipped for non-app URLs" do
        get "/latest.json"
        expect(response.status).to eq(200)
        expect(response.headers["Link"]).to eq(nil)
      end
    end

    context "when in preload mode" do
      before { global_setting :early_hint_header_mode, "preload" }

      it "includes the preload hint" do
        get "/latest"
        expect(response.status).to eq(200)
        expect(response.headers["Link"]).to include('.js>; rel="preload"')
        expect(response.headers["Link"]).to include('.css?__ws=test.localhost>; rel="preload"')
      end
    end
  end

  describe "preloading data" do
    def preloaded_json
      JSON.parse(Nokogiri::HTML5.fragment(response.body).css("script#data-preloaded").first.text)
    end

    context "when user is anon" do
      it "preloads the relevant JSON data" do
        get "/latest"
        expect(response.status).to eq(200)
        expect(preloaded_json.keys).to match_array(
          [
            "site",
            "siteSettings",
            "customHTML",
            "banner",
            "customEmoji",
            "isReadOnly",
            "isStaffWritesOnly",
            "activatedThemes",
            "#{TopicList.new("latest", Fabricate(:anonymous), []).preload_key}",
            "themeSiteSettingOverrides",
            "upcomingChanges",
          ],
        )
      end
    end

    context "when user is regular user" do
      fab!(:user)

      before { sign_in(user) }

      it "preloads the relevant JSON data" do
        get "/latest"
        expect(response.status).to eq(200)
        expect(preloaded_json.keys).to match_array(
          [
            "site",
            "siteSettings",
            "customHTML",
            "banner",
            "customEmoji",
            "isReadOnly",
            "isStaffWritesOnly",
            "activatedThemes",
            "#{TopicList.new("latest", Fabricate(:anonymous), []).preload_key}",
            "currentUser",
            "themeSiteSettingOverrides",
            "topicTrackingStates",
            "topicTrackingStateMeta",
            "upcomingChanges",
          ],
        )
      end
    end

    context "when user is admin" do
      fab!(:user, :admin)

      before { sign_in(user) }

      it "preloads the relevant JSON data" do
        get "/latest"
        expect(response.status).to eq(200)
        expect(preloaded_json.keys).to match_array(
          [
            "site",
            "siteSettings",
            "customHTML",
            "banner",
            "customEmoji",
            "isReadOnly",
            "isStaffWritesOnly",
            "activatedThemes",
            "#{TopicList.new("latest", Fabricate(:anonymous), []).preload_key}",
            "currentUser",
            "themeSiteSettingOverrides",
            "topicTrackingStates",
            "topicTrackingStateMeta",
            "fontMap",
            "visiblePlugins",
            "upcomingChanges",
          ],
        )
      end

      it "generates a fontMap" do
        get "/latest"
        expect(response.status).to eq(200)
        font_map = JSON.parse(preloaded_json["fontMap"])
        expect(font_map.keys).to match_array(
          DiscourseFonts.fonts.filter { |f| f[:variants].present? }.map { |f| f[:key] },
        )
      end

      it "has correctly loaded visiblePlugins" do
        get "/latest"
        expect(JSON.parse(preloaded_json["visiblePlugins"])).to eq([])
      end
    end

    describe "readonly serialization" do
      it "serializes regular readonly mode correctly" do
        Discourse.enable_readonly_mode(Discourse::USER_READONLY_MODE_KEY)

        get "/latest"
        expect(JSON.parse(preloaded_json["isReadOnly"])).to eq(true)
        expect(JSON.parse(preloaded_json["isStaffWritesOnly"])).to eq(false)
      ensure
        Discourse.disable_readonly_mode(Discourse::USER_READONLY_MODE_KEY)
      end

      it "serializes staff readonly mode correctly" do
        Discourse.enable_readonly_mode(Discourse::STAFF_WRITES_ONLY_MODE_KEY)

        get "/latest"
        expect(JSON.parse(preloaded_json["isReadOnly"])).to eq(true)
        expect(JSON.parse(preloaded_json["isStaffWritesOnly"])).to eq(true)
      ensure
        Discourse.disable_readonly_mode(Discourse::STAFF_WRITES_ONLY_MODE_KEY)
      end
    end
  end

  describe "#set_current_user_for_logs" do
    fab!(:admin)

    it "sets the X-Discourse-Username header for responses that cannot be stored" do
      sign_in(admin)

      get "/u/#{admin.username}.json"

      expect(response.status).to eq(200)
      expect(response.headers["Cache-Control"]).to include("no-store")
      expect(response.headers["X-Discourse-Username"]).to eq(admin.username)
    end

    it "sets the X-Discourse-Username header in bfcache compatibility mode" do
      # bfcache mode swaps `no-store` for `private`, which still keeps the
      # response out of any shared cache, so attribution is safe to keep.
      SiteSetting.cache_control_bfcache_compatibility = true
      sign_in(admin)

      get "/u/#{admin.username}.json"

      expect(response.status).to eq(200)
      expect(response.headers["Cache-Control"]).to eq("no-cache, private")
      expect(response.headers["X-Discourse-Username"]).to eq(admin.username)
    end

    it "sets the X-Discourse-Username header for private cached responses" do
      sign_in(admin)

      get "/manifest.webmanifest"

      expect(response.status).to eq(200)
      expect(response.headers["Cache-Control"]).to eq("max-age=60, private")
      expect(response.headers["X-Discourse-Username"]).to eq(admin.username)
    end

    it "sets the X-Discourse-Route header to the controller name and action including namespace" do
      sign_in(admin)

      get "/admin/users/#{admin.id}.json"
      expect(response.status).to eq(200)
      expect(response.headers["X-Discourse-Route"]).to eq("admin/users/show")

      get "/u/#{admin.username}.json"
      expect(response.status).to eq(200)
      expect(response.headers["X-Discourse-Route"]).to eq("users/show")
    end
  end

  describe "color definition stylesheets" do
    let!(:dark_scheme) { ColorScheme.find_by(base_scheme_id: ColorScheme::NAMES_TO_ID_MAP["Dark"]) }
    let!(:light_scheme) do
      ColorScheme.find_by(base_scheme_id: ColorScheme::NAMES_TO_ID_MAP["Solarized Light"])
    end

    before do
      Theme.find_default.update!(dark_color_scheme_id: dark_scheme.id)
      SiteSetting.interface_color_selector = "sidebar_footer"
    end

    context "when scheme cookies contain HTML" do
      it "does not add injected links to the page" do
        injected_link =
          '<link rel="modulepreload" data-plugin-name="poc" href="https://example.com/xss.js">'
        cookies[:color_scheme_id] = %(#{light_scheme.id}">#{injected_link})
        cookies[:dark_scheme_id] = %(#{dark_scheme.id}">#{injected_link})

        get "/"

        injected_links = css_select('link[rel="modulepreload"][data-plugin-name="poc"]')
        expect(injected_links).to be_empty
      end
    end

    context "with early hints" do
      before { global_setting :early_hint_header_mode, "preload" }

      it "includes stylesheet links in the header" do
        get "/"

        expect(response.headers["Link"]).to include("color_definitions_light-default")
        expect(response.headers["Link"]).to include("color_definitions_dark")
      end
    end

    context "when the default theme's scheme is the same as the site's default dark scheme" do
      before { Theme.find(SiteSetting.default_theme_id).update!(color_scheme_id: dark_scheme.id) }

      it "includes a single color stylesheet that has media=all" do
        get "/"

        color_stylesheets =
          css_select("link").select { |tag| tag[:href].include?("color_definitions") }

        expect(color_stylesheets.size).to eq(1)

        light_stylesheet = color_stylesheets.find { |tag| tag[:class] == "light-scheme" }
        expect(light_stylesheet[:media]).to eq("all")
      end
    end

    context "when light mode is forced" do
      before { cookies[:forced_color_mode] = "light" }

      it "includes a light stylesheet with media=all and a dark stylesheet with media=none" do
        get "/"

        color_stylesheets =
          css_select("link").select { |tag| tag[:href].include?("color_definitions") }

        expect(color_stylesheets.size).to eq(2)

        light_stylesheet = color_stylesheets.find { |tag| tag[:class] == "light-scheme" }
        dark_stylesheet = color_stylesheets.find { |tag| tag[:class] == "dark-scheme" }

        expect(light_stylesheet[:media]).to eq("all")
        expect(dark_stylesheet[:media]).to eq("none")
      end

      context "when the dark scheme no longer exists" do
        it "includes only a light stylesheet with media=all" do
          dark_scheme.destroy!
          get "/"

          color_stylesheets =
            css_select("link").select { |tag| tag[:href].include?("color_definitions") }

          expect(color_stylesheets.size).to eq(1)

          light_stylesheet = color_stylesheets.find { |tag| tag[:class] == "light-scheme" }

          expect(light_stylesheet[:media]).to eq("all")
        end
      end

      context "when all schemes are deleted" do
        it "includes only a light stylesheet with media=all" do
          ColorScheme.destroy_all
          get "/"

          color_stylesheets =
            css_select("link").select { |tag| tag[:href].include?("color_definitions") }

          expect(color_stylesheets.size).to eq(1)

          light_stylesheet = color_stylesheets.find { |tag| tag[:class] == "light-scheme" }

          expect(light_stylesheet[:media]).to eq("all")
        end
      end
    end

    context "when dark mode is forced" do
      before { cookies[:forced_color_mode] = "dark" }

      it "includes a light stylesheet with media=none and a dark stylesheet with media=all" do
        get "/"

        color_stylesheets =
          css_select("link").select { |tag| tag[:href].include?("color_definitions") }

        expect(color_stylesheets.size).to eq(2)

        light_stylesheet = color_stylesheets.find { |tag| tag[:class] == "light-scheme" }
        dark_stylesheet = color_stylesheets.find { |tag| tag[:class] == "dark-scheme" }

        expect(light_stylesheet[:media]).to eq("none")
        expect(dark_stylesheet[:media]).to eq("all")
      end

      context "when the dark scheme no longer exists" do
        it "includes only a light stylesheet with media=all" do
          dark_scheme.destroy!
          get "/"

          color_stylesheets =
            css_select("link").select { |tag| tag[:href].include?("color_definitions") }

          expect(color_stylesheets.size).to eq(1)

          light_stylesheet = color_stylesheets.find { |tag| tag[:class] == "light-scheme" }

          expect(light_stylesheet[:media]).to eq("all")
        end
      end

      context "when all schemes are deleted" do
        it "includes only a light stylesheet with media=all" do
          ColorScheme.destroy_all
          get "/"

          color_stylesheets =
            css_select("link").select { |tag| tag[:href].include?("color_definitions") }

          expect(color_stylesheets.size).to eq(1)

          light_stylesheet = color_stylesheets.find { |tag| tag[:class] == "light-scheme" }

          expect(light_stylesheet[:media]).to eq("all")
        end
      end
    end

    context "when color mode is automatic" do
      before { cookies[:forced_color_mode] = nil }

      it "includes a light stylesheet with media=(prefers-color-scheme: light) and a dark stylesheet with media=(prefers-color-scheme: dark)" do
        get "/"

        color_stylesheets =
          css_select("link").select { |tag| tag[:href].include?("color_definitions") }

        expect(color_stylesheets.size).to eq(2)

        light_stylesheet = color_stylesheets.find { |tag| tag[:class] == "light-scheme" }
        dark_stylesheet = color_stylesheets.find { |tag| tag[:class] == "dark-scheme" }

        expect(light_stylesheet[:media]).to eq("(prefers-color-scheme: light)")
        expect(dark_stylesheet[:media]).to eq("(prefers-color-scheme: dark)")
      end

      context "when the dark scheme no longer exists" do
        it "includes only a light stylesheet with media=all" do
          dark_scheme.destroy!
          get "/"

          color_stylesheets =
            css_select("link").select { |tag| tag[:href].include?("color_definitions") }

          expect(color_stylesheets.size).to eq(1)

          light_stylesheet = color_stylesheets.find { |tag| tag[:class] == "light-scheme" }

          expect(light_stylesheet[:media]).to eq("all")
        end
      end

      context "when all schemes are deleted" do
        it "includes only a light stylesheet with media=all" do
          ColorScheme.destroy_all
          get "/"

          color_stylesheets =
            css_select("link").select { |tag| tag[:href].include?("color_definitions") }

          expect(color_stylesheets.size).to eq(1)

          light_stylesheet = color_stylesheets.find { |tag| tag[:class] == "light-scheme" }

          expect(light_stylesheet[:media]).to eq("all")
        end
      end
    end
  end

  describe "google site verification" do
    it "is omitted by default" do
      get "/"
      expect(response.body).not_to include("google-site-verification")
    end

    it "is included when the site setting is set" do
      SiteSetting.google_site_verification_token = "verification_token"
      get "/"
      expect(response.body).to include(
        '<meta name="google-site-verification" content="verification_token">',
      )

      SiteSetting.login_required = true
      get "/"
      expect(response.body).to include(
        '<meta name="google-site-verification" content="verification_token">',
      )

      get "/", headers: { "User-Agent" => "Googlebot" }
      expect(response.body).to include(
        '<meta name="google-site-verification" content="verification_token">',
      )
    end
  end

  describe "when authorizing mini_profiler" do
    mini_profiler_stub = Class.new { def self.authorize_request = nil }

    around do |example|
      stub_const(ApplicationController, :MINI_PROFILER_CLASS, mini_profiler_stub) { example.run }
    end

    fab!(:developer) { Fabricate(:admin).tap { |u| Developer.create!(user_id: u.id) } }
    fab!(:user)
    fab!(:admin)

    before { allow(mini_profiler_stub).to receive(:authorize_request) }
    after { Developer.rebuild_cache }

    it "authorizes mini_profiler for developer user" do
      sign_in(developer)

      get "/latest"

      expect(mini_profiler_stub).to have_received(:authorize_request)
    end

    it "does not authorize mini_profiler for non-developer user" do
      sign_in(admin)

      get "/latest"

      expect(mini_profiler_stub).not_to have_received(:authorize_request)
    end

    describe "using the mini_profiler auth cookie" do
      def set_mini_profiler_auth_cookie(user, issued_at: Time.now.to_i)
        data = { user_id: user.id, issued_at: issued_at }
        jar = ActionDispatch::Cookies::CookieJar.build(ActionDispatch::TestRequest.create, {})
        jar.encrypted[:_mp_auth] = { value: data }
        cookies[:_mp_auth] = jar[:_mp_auth]
      end

      it "authorizes mini_profiler for anon user with valid cookie" do
        set_mini_profiler_auth_cookie(developer)

        get "/latest"

        expect(mini_profiler_stub).to have_received(:authorize_request)
      end

      it "does not authorize with expired cookie" do
        set_mini_profiler_auth_cookie(
          developer,
          issued_at:
            (ApplicationController::MINI_PROFILER_AUTH_COOKIE_EXPIRES_IN + 1.hour).ago.to_i,
        )

        get "/latest"

        expect(mini_profiler_stub).not_to have_received(:authorize_request)
      end

      it "does not authorize if user no longer exists" do
        set_mini_profiler_auth_cookie(developer)
        developer.destroy!

        get "/latest"

        expect(mini_profiler_stub).not_to have_received(:authorize_request)
      end

      it "does not authorize if user is no longer a developer" do
        set_mini_profiler_auth_cookie(developer)
        Developer.find_by(user_id: developer.id).destroy!

        get "/latest"

        expect(mini_profiler_stub).not_to have_received(:authorize_request)
      end
    end
  end
end
