# frozen_string_literal: true

require "rails_helper"

describe Doorkeeper::OpenidConnect::DiscoveryController, type: :controller do
  describe "#provider" do
    it "returns the provider configuration" do
      get :provider
      data = JSON.parse(response.body)

      expect(data.sort).to match({
        "issuer" => "dummy",
        "authorization_endpoint" => "http://test.host/oauth/authorize",
        "token_endpoint" => "http://test.host/oauth/token",
        "revocation_endpoint" => "http://test.host/oauth/revoke",
        "introspection_endpoint" => "http://test.host/oauth/introspect",
        "userinfo_endpoint" => "http://test.host/oauth/userinfo",
        "jwks_uri" => "http://test.host/oauth/discovery/keys",

        "scopes_supported" => ["openid"],
        "response_types_supported" => ["code", "token", "id_token", "id_token token"],
        "response_modes_supported" => %w[query fragment form_post],
        "grant_types_supported" => %w[authorization_code client_credentials implicit_oidc],

        "token_endpoint_auth_methods_supported" => %w[client_secret_basic client_secret_post],

        "subject_types_supported" => [
          "public",
        ],

        "id_token_signing_alg_values_supported" => [
          "RS256",
        ],

        "claim_types_supported" => [
          "normal",
        ],

        "claims_supported" => %w[
          iss
          sub
          aud
          exp
          iat
          name
          variable_name
          created_at
          updated_at
          token_id
          both_responses
          id_token_response
          user_info_response
        ],

        "code_challenge_methods_supported" => %w[
          plain
          S256
        ],
      }.sort)
    end

    it "returns the provider configuration with client registration url" do
      Doorkeeper::OpenidConnect.configure do
        issuer "dummy"
        dynamic_client_registration true
      end

      Rails.application.reload_routes!

      get :provider
      data = JSON.parse(response.body)

      expect(data.sort).to match({
        "issuer" => "dummy",
        "authorization_endpoint" => "http://test.host/oauth/authorize",
        "token_endpoint" => "http://test.host/oauth/token",
        "revocation_endpoint" => "http://test.host/oauth/revoke",
        "introspection_endpoint" => "http://test.host/oauth/introspect",
        "userinfo_endpoint" => "http://test.host/oauth/userinfo",
        "jwks_uri" => "http://test.host/oauth/discovery/keys",
        "registration_endpoint" => "http://test.host/oauth/registration",

        "scopes_supported" => ["openid"],
        "response_types_supported" => ["code", "token", "id_token", "id_token token"],
        "response_modes_supported" => %w[query fragment form_post],
        "grant_types_supported" => %w[authorization_code client_credentials implicit_oidc],

        "token_endpoint_auth_methods_supported" => %w[client_secret_basic client_secret_post],

        "subject_types_supported" => [
          "public",
        ],

        "id_token_signing_alg_values_supported" => [
          "RS256",
        ],

        "claim_types_supported" => [
          "normal",
        ],

        "claims_supported" => %w[
          iss
          sub
          aud
          exp
          iat
        ],

        "code_challenge_methods_supported" => %w[
          plain
          S256
        ],
      }.sort)
    end

    context "when refresh_token grant type is enabled" do
      before { Doorkeeper.configure { use_refresh_token } }

      it "add refresh_token to grant_types_supported" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["grant_types_supported"]).to eq %w[authorization_code client_credentials refresh_token]
      end
    end

    context "when issuer block" do
      before { Doorkeeper::OpenidConnect.configure { issuer { |_r, _a| "test-issuer" } } }

      it "return blocks result" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["issuer"]).to eq "test-issuer"
      end
    end

    context "when issuer block has arity 3 with request" do
      before do
        Doorkeeper::OpenidConnect.configure do
          issuer do |_resource_owner, _application, request|
            request.base_url
          end
        end
      end

      it "passes the request object to the issuer block" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["issuer"]).to eq "http://test.host"
      end
    end

    context "when grant_flows is configured with only client_credentials" do
      before { Doorkeeper.configure { grant_flows %w[client_credentials] } }

      it "return empty response_modes_supported" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["response_modes_supported"]).to eq []
      end
    end

    context "when pkce_code_challenge_methods is configured with only S256",
            if: Doorkeeper::Config.method_defined?(:pkce_code_challenge_methods) do
      before { Doorkeeper.configure { pkce_code_challenge_methods %w[S256] } }

      it "return only S256 in code_challenge_methods_supported" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["code_challenge_methods_supported"]).to eq %w[S256]
      end
    end

    context "when pkce_code_challenge_methods is configured with only plain",
            if: Doorkeeper::Config.method_defined?(:pkce_code_challenge_methods) do
      before { Doorkeeper.configure { pkce_code_challenge_methods %w[plain] } }

      it "return only plain in code_challenge_methods_supported" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["code_challenge_methods_supported"]).to eq %w[plain]
      end
    end

    context "when grant_flows is configured only implicit flow" do
      before { Doorkeeper.configure { grant_flows %w[implicit_oidc] } }

      it "return fragment and form_post as response_modes_supported" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["response_modes_supported"]).to eq %w[fragment form_post]
      end
    end

    context "when client_credentials is configured with only from_basic" do
      before { Doorkeeper.configure { client_credentials :from_basic } }

      it "returns only client_secret_basic in token_endpoint_auth_methods_supported" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["token_endpoint_auth_methods_supported"]).to eq %w[client_secret_basic]
      end
    end

    context "when client_credentials is configured with only from_params" do
      before { Doorkeeper.configure { client_credentials :from_params } }

      it "returns only client_secret_post in token_endpoint_auth_methods_supported" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["token_endpoint_auth_methods_supported"]).to eq %w[client_secret_post]
      end
    end

    context "when the issuer is configured to a non-root URL" do
      let(:non_root_issuer) { "http://test.host/issuer/with/path" }

      before do
        value = non_root_issuer
        Doorkeeper::OpenidConnect.configure do
          issuer value
        end
      end

      it "returns the configured issuer in the webfinger response" do
        get :webfinger, params: { resource: "user@example.com" }
        data = JSON.parse(response.body)

        expect(data["links"].first["href"]).to eq non_root_issuer
      end
    end

    context "when client_credentials is configured with both from_basic and from_params" do
      before { Doorkeeper.configure { client_credentials :from_basic, :from_params } }

      it "returns both client_secret_basic and client_secret_post in token_endpoint_auth_methods_supported" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["token_endpoint_auth_methods_supported"]).to eq %w[client_secret_basic client_secret_post]
      end
    end

    context "when grant_flows is configured with authorization_code and implicit flow" do
      before { Doorkeeper.configure { grant_flows %w[authorization_code implicit_oidc] } }

      it "return query, fragment and form_post as response_modes_supported" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["response_modes_supported"]).to eq %w[query fragment form_post]
      end
    end

    it "uses the protocol option for generating URLs" do
      Doorkeeper::OpenidConnect.configure do
        issuer "dummy"
        protocol { :testing }
      end

      get :provider
      data = JSON.parse(response.body)

      expect(data["authorization_endpoint"]).to eq "testing://test.host/oauth/authorize"
    end

    context "when the protocol option is configured with a non-callable value" do
      it "accepts a symbol" do
        Doorkeeper::OpenidConnect.configure do
          issuer "dummy"
          protocol :testing
        end

        get :provider
        data = JSON.parse(response.body)

        expect(response).to have_http_status(:ok)
        expect(data["authorization_endpoint"]).to eq "testing://test.host/oauth/authorize"
      end

      it "accepts a string" do
        Doorkeeper::OpenidConnect.configure do
          issuer "dummy"
          protocol "testing"
        end

        get :provider
        data = JSON.parse(response.body)

        expect(response).to have_http_status(:ok)
        expect(data["authorization_endpoint"]).to eq "testing://test.host/oauth/authorize"
      end
    end

    context "when the discovery_url_options option is set for all endpoints" do
      before do
        Doorkeeper::OpenidConnect.configure do
          issuer "dummy"
          discovery_url_options do |_request|
            {
              authorization: { host: "alternate-authorization.host" },
              token: { host: "alternate-token.host" },
              revocation: { host: "alternate-revocation.host" },
              introspection: { host: "alternate-introspection.host" },
              userinfo: { host: "alternate-userinfo.host" },
              jwks: { host: "alternate-jwks.host" },
            }
          end
        end
      end

      it "uses the discovery_url_options option when generating the endpoint urls" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["authorization_endpoint"]).to eq "http://alternate-authorization.host/oauth/authorize"
        expect(data["token_endpoint"]).to eq "http://alternate-token.host/oauth/token"
        expect(data["revocation_endpoint"]).to eq "http://alternate-revocation.host/oauth/revoke"
        expect(data["introspection_endpoint"]).to eq "http://alternate-introspection.host/oauth/introspect"
        expect(data["userinfo_endpoint"]).to eq "http://alternate-userinfo.host/oauth/userinfo"
        expect(data["jwks_uri"]).to eq "http://alternate-jwks.host/oauth/discovery/keys"
      end
    end

    context "when the discovery_url_options option is only set for some endpoints" do
      before do
        Doorkeeper::OpenidConnect.configure do
          issuer "dummy"
          discovery_url_options do |_request|
            { authorization: { host: "alternate-authorization.host" } }
          end
        end
      end

      it "does not use the discovery_url_options option when generating other URLs" do
        get :provider
        data = JSON.parse(response.body)

        {
          "token_endpoint" => "http://test.host/oauth/token",
          "revocation_endpoint" => "http://test.host/oauth/revoke",
          "introspection_endpoint" => "http://test.host/oauth/introspect",
          "userinfo_endpoint" => "http://test.host/oauth/userinfo",
          "jwks_uri" => "http://test.host/oauth/discovery/keys",
        }.each do |endpoint, expected_url|
          expect(data[endpoint]).to eq expected_url
        end
      end
    end

    it "does not return an end session endpoint if none is configured" do
      get :provider
      data = JSON.parse(response.body)

      expect(data.key?("end_session_endpoint")).to be(false)
    end

    it "uses the configured end session endpoint with self as context" do
      Doorkeeper::OpenidConnect.configure do
        issuer "dummy"
        end_session_endpoint -> { logout_url }
      end

      def controller.logout_url
        "http://test.host/logout"
      end

      get :provider
      data = JSON.parse(response.body)

      expect(data["end_session_endpoint"]).to eq "http://test.host/logout"
    end

    context "when token inspection is disallowed" do
      let(:doorkeeper_config) { Doorkeeper.config }
      let!(:allow_token_introspection) { doorkeeper_config.allow_token_introspection }

      before do
        allow(doorkeeper_config).to receive(:allow_token_introspection).and_return(false)
        Rails.application.reload_routes!
      end

      after do
        allow(doorkeeper_config).to receive(:allow_token_introspection).and_return(allow_token_introspection)
        Rails.application.reload_routes!
      end

      it "does not return introspection_endpoint" do
        get :provider
        data = JSON.parse(response.body)

        expect(data.key?("introspection_endpoint")).to be(false)
      end
    end
  end

  describe "#webfinger" do
    it "requires the resource parameter" do
      expect do
        get :webfinger
      end.to raise_error ActionController::ParameterMissing
    end

    it "returns the OpenID Connect relation" do
      get :webfinger, params: { resource: "user@example.com" }
      data = JSON.parse(response.body)

      expect(data.sort).to eq({
        "subject" => "user@example.com",
        "links" => [
          {
            "rel" => "http://openid.net/specs/connect/1.0/issuer",
            "href" => "dummy",
          },
        ],
      }.sort)
    end

    context "when the discovery_url_options option uses the request for an endpoint" do
      before do
        Doorkeeper::OpenidConnect.configure do
          issuer "dummy"
          discovery_url_options do |request|
            {
              authorization: { host: "alternate-authorization.host",
                               protocol: request.ssl? ? :https : :testing, },
            }
          end
        end
      end

      it "uses the discovery_url_options option when generating the webfinger endpoint url" do
        get :provider
        data = JSON.parse(response.body)

        expect(data["authorization_endpoint"]).to eq "testing://alternate-authorization.host/oauth/authorize"
      end
    end
  end

  describe "#keys" do
    subject { get :keys }

    shared_examples "a key response" do |options|
      expected_parameters = options[:expected_parameters]

      it "includes only #{expected_parameters.join(", ")} parameters" do
        subject
        data = JSON.parse(response.body)
        key = data["keys"].first

        expect(key.keys.map(&:to_sym)).to match_array(expected_parameters)
      end
    end

    context "when using an RSA key" do
      it_behaves_like "a key response", expected_parameters: %i[kty kid e n use alg]
    end

    context "when using an EC key" do
      before { configure_ec }

      it_behaves_like "a key response", expected_parameters: %i[kty kid crv x y use alg]
    end

    context "when using an HMAC key" do
      before { configure_hmac }

      it_behaves_like "a key response", expected_parameters: %i[kty kid use alg]
    end

    context "when multiple signing keys are configured" do
      let(:rsa_pem1) { OpenSSL::PKey::RSA.generate(2048).to_pem }
      let(:rsa_pem2) { OpenSSL::PKey::RSA.generate(2048).to_pem }

      before do
        keys = [rsa_pem1, rsa_pem2]
        Doorkeeper::OpenidConnect.configure do
          issuer "dummy"
          signing_key keys
        end
      end

      it "publishes one JWKS entry per configured key" do
        subject
        data = JSON.parse(response.body)

        expect(data["keys"].size).to eq 2
        expect(data["keys"]).to all(include("use" => "sig", "alg" => "RS256"))
      end

      it "lists the active signing key first" do
        subject
        data = JSON.parse(response.body)

        expect(data["keys"].first["kid"]).to eq Doorkeeper::OpenidConnect.signing_key.kid
      end

      it "produces distinct kids per key" do
        subject
        data = JSON.parse(response.body)

        kids = data["keys"].map { |k| k["kid"] }
        expect(kids.uniq).to eq kids
      end
    end
  end
end
