# frozen_string_literal: true
require "spec_helper"

module SecureHeaders
  describe PolicyManagement do
    before(:each) do
      reset_config
      Configuration.default
    end

    let(:default_opts) do
      {
        default_src: %w(https:),
        img_src: %w(https: data:),
        script_src: %w('unsafe-inline' 'unsafe-eval' https: data:),
        style_src: %w('unsafe-inline' https: about:),
        report_uri: %w(/csp_report)
      }
    end

    describe "#validate_config!" do
      it "accepts all keys" do
        # (pulled from README)
        config = {
          # "meta" values. these will shape the header, but the values are not included in the header.
          report_only:  false,
          preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.

          # directive values: these values will directly translate into source directives
          default_src: %w(https: 'self'),

          base_uri: %w('self'),
          connect_src: %w(wss:),
          child_src: %w('self' *.twimg.com itunes.apple.com),
          font_src: %w('self' data:),
          form_action: %w('self' github.com),
          frame_ancestors: %w('none'),
          frame_src: %w('self' *.twimg.com itunes.apple.com),
          img_src: %w(mycdn.com data:),
          manifest_src: %w(manifest.com),
          media_src: %w(utoob.com),
          navigate_to: %w(netscape.com),
          object_src: %w('self'),
          plugin_types: %w(application/x-shockwave-flash),
          prefetch_src: %w(fetch.com),
          require_sri_for: %w(script style),
          require_trusted_types_for: %w('script'),
          script_src: %w('self'),
          style_src: %w('unsafe-inline'),
          upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
          worker_src: %w(worker.com),
          script_src_elem: %w(example.com),
          script_src_attr: %w(example.com),
          style_src_elem: %w(example.com),
          style_src_attr: %w(example.com),
          trusted_types: %w(abcpolicy),

          report_uri: %w(https://example.com/uri-directive),
        }

        ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(config))
      end

      it "requires a :default_src value" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(script_src: %w('self')))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "requires a :script_src value" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self')))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "accepts OPT_OUT as a script-src value" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: OPT_OUT))
        end.to_not raise_error
      end

      it "requires :report_only to be a truthy value" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: "steve")))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "requires :preserve_schemes to be a truthy value" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(preserve_schemes: "steve")))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "requires :upgrade_insecure_requests to be a boolean value" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(upgrade_insecure_requests: "steve")))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "requires all source lists to be an array of strings" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: "steve"))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "allows nil values" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: ["https:", nil]))
        end.to_not raise_error
      end

      it "rejects unknown directives / config" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), default_src_totally_mispelled: "steve"))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "rejects style for trusted types" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(style_src: %w('self'), require_trusted_types_for: %w(script style), trusted_types: %w(abcpolicy))))
        end
      end

      # this is mostly to ensure people don't use the antiquated shorthands common in other configs
      it "performs light validation on source lists" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval), script_src: %w('self')))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "rejects anything not of the form allow-* as a sandbox value" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["steve"])))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "accepts anything of the form allow-* as a sandbox value " do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["allow-foo"])))
        end.to_not raise_error
      end

      it "accepts true as a sandbox policy" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: true)))
        end.to_not raise_error
      end

      it "rejects anything not of the form type/subtype as a plugin-type value" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["steve"])))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "accepts anything of the form type/subtype as a plugin-type value " do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["application/pdf"])))
        end.to_not raise_error
      end

      it "doesn't allow report_only to be set in a non-report-only config" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: true)))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "allows report_only to be set in a report-only config" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyReportOnlyConfig.new(default_opts.merge(report_only: true)))
        end.to_not raise_error
      end

      it "requires report_to to be a string" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_to: ["endpoint"])))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "rejects empty report_to endpoint names" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_to: "")))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end

      it "accepts valid report_to endpoint names" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_to: "csp-endpoint")))
        end.to_not raise_error
      end

      it "accepts report_to with hyphens and underscores" do
        expect do
          ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_to: "csp-endpoint_name-123")))
        end.to_not raise_error
      end
    end

    describe "#combine_policies" do
      before(:each) do
        reset_config
      end
      it "combines the default-src value with the override if the directive was unconfigured" do
        Configuration.default do |config|
          config.csp = {
            default_src: %w(https:),
            script_src: %w('self'),
          }
        end
        default_policy = Configuration.dup
        combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, style_src: %w(anothercdn.com))
        csp = ContentSecurityPolicy.new(combined_config)
        expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME)
        expect(csp.value).to eq("default-src https:; script-src 'self'; style-src https: anothercdn.com")
      end

      it "combines directives where the original value is nil and the hash is frozen" do
        Configuration.default do |config|
          config.csp = {
            default_src: %w('self'),
            script_src: %w('self'),
            report_only: false
          }.freeze
        end
        report_uri = "https://report-uri.io/asdf"
        default_policy = Configuration.dup
        combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_uri: [report_uri])
        csp = ContentSecurityPolicy.new(combined_config)
        expect(csp.value).to include("report-uri #{report_uri}")
      end

      it "does not combine the default-src value for directives that don't fall back to default sources" do
        Configuration.default do |config|
          config.csp = {
            default_src: %w('self'),
            script_src: %w('self'),
            report_only: false
          }.freeze
        end
        non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash|
          hash[directive] = %w("http://example.org)
        end
        default_policy = Configuration.dup
        combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, non_default_source_additions)

        ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive|
          expect(combined_config[directive]).to eq(%w("http://example.org))
        end
      end

      it "overrides the report_only flag" do
        Configuration.default do |config|
          config.csp = {
            default_src: %w('self'),
            script_src: %w('self'),
            report_only: false
          }
        end
        default_policy = Configuration.dup
        combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_only: true)
        csp = ContentSecurityPolicy.new(combined_config)
        expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME)
      end

      it "overrides the :upgrade_insecure_requests flag" do
        Configuration.default do |config|
          config.csp = {
            default_src: %w(https:),
            script_src: %w('self'),
            upgrade_insecure_requests: false
          }
        end
        default_policy = Configuration.dup
        combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, upgrade_insecure_requests: true)
        csp = ContentSecurityPolicy.new(combined_config)
        expect(csp.value).to eq("default-src https:; script-src 'self'; upgrade-insecure-requests")
      end

      it "raises an error if appending to a OPT_OUT policy" do
        Configuration.default do |config|
          config.csp = OPT_OUT
        end
        default_policy = Configuration.dup
        expect do
          ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, script_src: %w(anothercdn.com))
        end.to raise_error(ContentSecurityPolicyConfigError)
      end
    end
  end
end
