# frozen_string_literal: true

require "spec_helper"

describe Doorkeeper::AuthorizationsController, "implicit grant flow" do
  include AuthorizationRequestHelper

  class ActionDispatch::TestResponse
    def query_params
      @query_params ||= begin
        fragment = URI.parse(location).fragment
        Rack::Utils.parse_query(fragment)
      end
    end
  end

  let(:client)        { FactoryBot.create :application }
  let(:user)          { User.create!(name: "Joe", password: "sekret") }
  let(:access_token)  { FactoryBot.build :access_token, resource_owner_id: user.id, application_id: client.id, scopes: "default" }

  before do
    Doorkeeper.configure do
      default_scopes :default

      custom_access_token_expires_in(lambda do |context|
        context.grant_type == Doorkeeper::OAuth::IMPLICIT ? 1234 : nil
      end)
    end

    allow(Doorkeeper.config).to receive(:grant_flows).and_return(["implicit"])
    allow(Doorkeeper.config).to receive(:authenticate_resource_owner).and_return(->(_) { authenticator_method })
    allow(controller).to receive(:authenticator_method).and_return(user)
    expect(controller).to receive(:authenticator_method).at_most(:once)
  end

  describe "POST #create" do
    before do
      post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri }
    end

    it "redirects after authorization" do
      expect(response).to be_redirect
    end

    it "redirects to client redirect uri" do
      expect(response.location).to match(/^#{client.redirect_uri}/)
    end

    it "includes access token in fragment" do
      expect(response.query_params["access_token"]).to eq(Doorkeeper::AccessToken.first.token)
    end

    it "includes token type in fragment" do
      expect(response.query_params["token_type"]).to eq("Bearer")
    end

    it "includes token expiration in fragment" do
      expect(response.query_params["expires_in"].to_i).to eq(1234)
    end

    it "issues the token for the current client" do
      expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id)
    end

    it "issues the token for the current resource owner" do
      expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id)
    end
  end

  describe "POST #create in API mode" do
    before do
      allow(Doorkeeper.config).to receive(:api_only).and_return(true)
      post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri }
    end

    let(:response_json_body) { JSON.parse(response.body) }
    let(:redirect_uri) { response_json_body["redirect_uri"] }

    it "renders success after authorization" do
      expect(response).to be_successful
    end

    it "renders correct redirect uri" do
      expect(redirect_uri).to match(/^#{client.redirect_uri}/)
    end

    it "includes access token in fragment" do
      expect(redirect_uri.match(/access_token=([a-zA-Z0-9\-_]+)&?/)[1]).to eq(Doorkeeper::AccessToken.first.token)
    end

    it "includes token type in fragment" do
      expect(redirect_uri.match(/token_type=(\w+)&?/)[1]).to eq "Bearer"
    end

    it "includes token expiration in fragment" do
      expect(redirect_uri.match(/expires_in=(\d+)&?/)[1].to_i).to eq 1234
    end

    it "issues the token for the current client" do
      expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id)
    end

    it "issues the token for the current resource owner" do
      expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id)
    end
  end

  describe "POST #create with errors" do
    context "when missing client_id" do
      before do
        post :create, params: {
          client_id: "",
          response_type: "token",
          redirect_uri: client.redirect_uri,
        }
      end

      let(:response_json_body) { JSON.parse(response.body) }

      it "renders 400 error" do
        expect(response.status).to eq 400
      end

      it "includes error name" do
        expect(response_json_body["error"]).to eq("invalid_request")
      end

      it "includes error description" do
        expect(response_json_body["error_description"]).to eq(
          translated_invalid_request_error_message(:missing_param, :client_id),
        )
      end

      it "does not issue any access token" do
        expect(Doorkeeper::AccessToken.all).to be_empty
      end
    end

    context "when other error happens" do
      before do
        default_scopes_exist :public

        post :create, params: {
          client_id: client.uid,
          response_type: "token",
          scope: "invalid",
          redirect_uri: client.redirect_uri,
        }
      end

      it "redirects after authorization" do
        expect(response).to be_redirect
      end

      it "redirects to client redirect uri" do
        expect(response.location).to match(/^#{client.redirect_uri}/)
      end

      it "does not include access token in fragment" do
        expect(response.query_params["access_token"]).to be_nil
      end

      it "includes error in fragment" do
        expect(response.query_params["error"]).to eq("invalid_scope")
      end

      it "includes error description in fragment" do
        expect(response.query_params["error_description"]).to eq(translated_error_message(:invalid_scope))
      end

      it "does not issue any access token" do
        expect(Doorkeeper::AccessToken.all).to be_empty
      end
    end
  end

  describe "POST #create in API mode with errors" do
    context "when missing client_id" do
      before do
        allow(Doorkeeper.config).to receive(:api_only).and_return(true)

        post :create, params: {
          client_id: "",
          response_type: "token",
          redirect_uri: client.redirect_uri,
        }
      end

      let(:response_json_body) { JSON.parse(response.body) }

      it "renders 400 error" do
        expect(response.status).to eq 400
      end

      it "includes error name" do
        expect(response_json_body["error"]).to eq("invalid_request")
      end

      it "includes error description" do
        expect(response_json_body["error_description"]).to eq(
          translated_invalid_request_error_message(:missing_param, :client_id),
        )
      end

      it "does not issue any access token" do
        expect(Doorkeeper::AccessToken.all).to be_empty
      end
    end

    context "when other error happens" do
      before do
        allow(Doorkeeper.config).to receive(:api_only).and_return(true)
        default_scopes_exist :public

        post :create, params: {
          client_id: client.uid,
          response_type: "token",
          scope: "invalid",
          redirect_uri: client.redirect_uri,
        }
      end

      let(:response_json_body) { JSON.parse(response.body) }
      let(:redirect_uri) { response_json_body["redirect_uri"] }

      it "renders 400 error" do
        expect(response.status).to eq 400
      end

      it "includes correct redirect URI" do
        expect(redirect_uri).to match(/^#{client.redirect_uri}/)
      end

      it "does not include access token in fragment" do
        expect(redirect_uri.match(/access_token=([a-f0-9]+)&?/)).to be_nil
      end

      it "includes error in redirect uri" do
        expect(redirect_uri.match(/error=([a-z_]+)&?/)[1]).to eq "invalid_scope"
      end

      it "includes error description in redirect uri" do
        expect(redirect_uri.match(/error_description=(.+)&?/)[1]).to_not be_nil
      end

      it "does not issue any access token" do
        expect(Doorkeeper::AccessToken.all).to be_empty
      end
    end
  end

  describe "POST #create with application already authorized" do
    before do
      allow(Doorkeeper.config).to receive(:reuse_access_token).and_return(true)

      access_token.save!

      post :create, params: {
        client_id: client.uid,
        response_type: "token",
        redirect_uri: client.redirect_uri,
      }
    end

    it "returns the existing access token in a fragment" do
      expect(response.query_params["access_token"]).to eq(access_token.token)
    end

    it "does not creates a new access token" do
      expect(Doorkeeper::AccessToken.count).to eq(1)
    end
  end

  describe "POST #create with callbacks" do
    after do
      client.update_attribute :redirect_uri, "urn:ietf:wg:oauth:2.0:oob"
    end

    describe "when successful" do
      after do
        post :create, params: {
          client_id: client.uid,
          response_type: "token",
          redirect_uri: client.redirect_uri,
        }
      end

      it "should call :before_successful_authorization callback" do
        expect(Doorkeeper.config)
          .to receive_message_chain(:before_successful_authorization, :call).with(instance_of(described_class))
      end

      it "should call :after_successful_authorization callback" do
        expect(Doorkeeper.config)
          .to receive_message_chain(:after_successful_authorization, :call).with(instance_of(described_class))
      end
    end

    describe "with errors" do
      after do
        post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: "bad_uri" }
      end

      it "should not call :before_successful_authorization callback" do
        expect(Doorkeeper.config).not_to receive(:before_successful_authorization)
      end

      it "should not call :after_successful_authorization callback" do
        expect(Doorkeeper.config).not_to receive(:after_successful_authorization)
      end
    end
  end

  describe "GET #new token request with native url and skip_authorization true" do
    before do
      allow(Doorkeeper.config).to receive(:skip_authorization).and_return(proc do
        true
      end)

      client.update_attribute :redirect_uri, "urn:ietf:wg:oauth:2.0:oob"

      get :new, params: {
        client_id: client.uid,
        response_type: "token",
        redirect_uri: client.redirect_uri,
      }
    end

    it "should redirect immediately" do
      expect(response).to be_redirect
      expect(response.location).to match(%r{/oauth/token/info\?access_token=})
    end

    it "should not issue a grant" do
      expect(Doorkeeper::AccessGrant.count).to be 0
    end

    it "should issue a token" do
      expect(Doorkeeper::AccessToken.count).to be 1
    end
  end

  describe "GET #new code request with native url and skip_authorization true" do
    before do
      allow(Doorkeeper.config).to receive(:grant_flows).and_return(%w[authorization_code])
      allow(Doorkeeper.config).to receive(:skip_authorization).and_return(proc do
        true
      end)

      client.update_attribute :redirect_uri, "urn:ietf:wg:oauth:2.0:oob"

      get :new, params: {
        client_id: client.uid,
        response_type: "code",
        redirect_uri: client.redirect_uri,
      }
    end

    it "should redirect immediately" do
      expect(response).to be_redirect
      expect(response.location)
        .to match(%r{/oauth/authorize/native\?code=#{Doorkeeper::AccessGrant.first.token}})
    end

    it "should issue a grant" do
      expect(Doorkeeper::AccessGrant.count).to be 1
    end

    it "should not issue a token" do
      expect(Doorkeeper::AccessToken.count).to be 0
    end
  end

  describe "GET #new with skip_authorization true" do
    before do
      allow(Doorkeeper.config).to receive(:skip_authorization).and_return(proc do
        true
      end)

      get :new, params: {
        client_id: client.uid,
        response_type: "token",
        redirect_uri: client.redirect_uri,
      }
    end

    it "should redirect immediately" do
      expect(response).to be_redirect
      expect(response.location).to match(/^#{client.redirect_uri}/)
    end

    it "should issue a token" do
      expect(Doorkeeper::AccessToken.count).to be 1
    end

    it "includes token type in fragment" do
      expect(response.query_params["token_type"]).to eq("Bearer")
    end

    it "includes token expiration in fragment" do
      expect(response.query_params["expires_in"].to_i).to eq(1234)
    end

    it "issues the token for the current client" do
      expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id)
    end

    it "issues the token for the current resource owner" do
      expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id)
    end
  end

  describe "GET #new in API mode" do
    before do
      allow(Doorkeeper.config).to receive(:api_only).and_return(true)

      get :new, params: {
        client_id: client.uid,
        response_type: "token",
        redirect_uri: client.redirect_uri,
      }
    end

    it "should render success" do
      expect(response).to be_successful
    end

    it "sets status to pre-authorization" do
      expect(json_response["status"]).to eq(I18n.t("doorkeeper.pre_authorization.status"))
    end

    it "sets correct values" do
      expect(json_response["client_id"]).to eq(client.uid)
      expect(json_response["redirect_uri"]).to eq(client.redirect_uri)
      expect(json_response["state"]).to be_nil
      expect(json_response["response_type"]).to eq("token")
      expect(json_response["scope"]).to eq("default")
    end
  end

  describe "GET #new in API mode with skip_authorization true" do
    before do
      allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc { true })
      allow(Doorkeeper.configuration).to receive(:api_only).and_return(true)

      get :new, params: {
        client_id: client.uid,
        response_type: "token",
        redirect_uri: client.redirect_uri,
      }
    end

    it "should render success" do
      expect(response).to be_successful
    end

    it "should issue a token" do
      expect(Doorkeeper::AccessToken.count).to be 1
    end

    it "sets status to redirect" do
      expect(JSON.parse(response.body)["status"]).to eq("redirect")
    end

    it "sets redirect_uri to correct value" do
      redirect_uri = JSON.parse(response.body)["redirect_uri"]
      expect(redirect_uri).to_not be_nil
      expect(redirect_uri.match(/token_type=(\w+)&?/)[1]).to eq "Bearer"
      expect(redirect_uri.match(/expires_in=(\d+)&?/)[1].to_i).to eq 1234
      expect(
        redirect_uri.match(/access_token=([a-zA-Z0-9\-_]+)&?/)[1],
      ).to eq Doorkeeper::AccessToken.first.token
    end

    it "issues the token for the current client" do
      expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id)
    end

    it "issues the token for the current resource owner" do
      expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id)
    end
  end

  describe "GET #new with errors" do
    before do
      default_scopes_exist :public
      get :new, params: { an_invalid: "request" }
    end

    it "does not redirect" do
      expect(response).to_not be_redirect
    end

    it "does not issue any token" do
      expect(Doorkeeper::AccessGrant.count).to eq 0
      expect(Doorkeeper::AccessToken.count).to eq 0
    end
  end

  describe "GET #new in API mode with errors" do
    let(:response_json_body) { JSON.parse(response.body) }

    before do
      default_scopes_exist :public
      allow(Doorkeeper.configuration).to receive(:api_only).and_return(true)
      get :new, params: { an_invalid: "request" }
    end

    it "should render bad request" do
      expect(response).to have_http_status(:bad_request)
    end

    it "includes error in body" do
      expect(response_json_body["error"]).to eq("invalid_request")
    end

    it "includes error description in body" do
      expect(response_json_body["error_description"])
        .to eq(translated_invalid_request_error_message(:missing_param, :client_id))
    end

    it "does not issue any token" do
      expect(Doorkeeper::AccessGrant.count).to eq 0
      expect(Doorkeeper::AccessToken.count).to eq 0
    end
  end

  describe "GET #new with callbacks" do
    after do
      client.update_attribute :redirect_uri, "urn:ietf:wg:oauth:2.0:oob"
      get :new, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri }
    end

    describe "when authorizing" do
      before do
        allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc { true })
      end

      it "should call :before_successful_authorization callback" do
        expect(Doorkeeper.configuration)
          .to receive_message_chain(:before_successful_authorization, :call).with(instance_of(described_class))
      end

      it "should call :after_successful_authorization callback" do
        expect(Doorkeeper.configuration)
          .to receive_message_chain(:after_successful_authorization, :call).with(instance_of(described_class))
      end
    end

    describe "when not authorizing" do
      before do
        allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc { false })
      end

      it "should not call :before_successful_authorization callback" do
        expect(Doorkeeper.configuration).not_to receive(:before_successful_authorization)
      end

      it "should not call :after_successful_authorization callback" do
        expect(Doorkeeper.configuration).not_to receive(:after_successful_authorization)
      end
    end

    describe "when not authorizing in api mode" do
      before do
        allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc { false })
        allow(Doorkeeper.configuration).to receive(:api_only).and_return(true)
      end

      it "should not call :before_successful_authorization callback" do
        expect(Doorkeeper.configuration).not_to receive(:before_successful_authorization)
      end

      it "should not call :after_successful_authorization callback" do
        expect(Doorkeeper.configuration).not_to receive(:after_successful_authorization)
      end
    end
  end

  describe "authorize response memoization" do
    it "memoizes the result of the authorization" do
      pre_auth = double(:pre_auth, authorizable?: true)
      allow(controller).to receive(:pre_auth) { pre_auth }
      strategy = double(:strategy, authorize: true)
      expect(strategy).to receive(:authorize).once
      allow(controller).to receive(:strategy) { strategy }
      allow(controller).to receive(:create) do
        2.times { controller.send :authorize_response }
        controller.render json: {}, status: :ok
      end

      post :create
    end
  end

  describe "strong parameters" do
    it "ignores non-scalar scope parameter" do
      get :new, params: {
        client_id: client.uid,
        response_type: "token",
        redirect_uri: client.redirect_uri,
        scope: { "0" => "profile" },
      }

      expect(response).to be_successful
    end
  end
end
