# frozen_string_literal: true

require "spec_helper"

describe Doorkeeper::OAuth::RefreshTokenRequest do
  let(:server) do
    double :server,
           access_token_expires_in: 2.minutes
  end

  let(:refresh_token) do
    FactoryBot.create(:access_token, use_refresh_token: true)
  end

  let(:client) { refresh_token.application }
  let(:credentials) { Doorkeeper::OAuth::Client::Credentials.new(client.uid, client.secret) }

  before do
    allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(false)
    allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(false)
  end

  subject { described_class.new server, refresh_token, credentials }

  it "issues a new token for the client" do
    expect { subject.authorize }.to change { client.reload.access_tokens.count }.by(1)
    # #sort_by used for MongoDB ORM extensions for valid ordering
    expect(client.reload.access_tokens.max_by(&:created_at).expires_in).to eq(120)
  end

  it "issues a new token for the client with custom expires_in" do
    server = double :server,
                    access_token_expires_in: 2.minutes,
                    custom_access_token_expires_in: lambda { |context|
                      context.grant_type == Doorkeeper::OAuth::REFRESH_TOKEN ? 1234 : nil
                    }

    allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true)
    allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(false)

    described_class.new(server, refresh_token, credentials).authorize

    # #sort_by used for MongoDB ORM extensions for valid ordering
    expect(client.reload.access_tokens.max_by(&:created_at).expires_in).to eq(1234)
  end

  it "revokes the previous token" do
    expect { subject.authorize }.to change { refresh_token.revoked? }.from(false).to(true)
  end

  it "calls configured request callback methods" do
    expect(Doorkeeper.configuration.before_successful_strategy_response)
      .to receive(:call).with(subject).once

    expect(Doorkeeper.configuration.after_successful_strategy_response)
      .to receive(:call).with(subject, instance_of(Doorkeeper::OAuth::TokenResponse)).once

    subject.authorize
  end

  it "requires the refresh token" do
    subject.refresh_token = nil
    subject.validate
    expect(subject.error).to eq(:invalid_request)
    expect(subject.missing_param).to eq(:refresh_token)
  end

  it "requires credentials to be valid if provided" do
    subject.client = nil
    subject.validate
    expect(subject.error).to eq(:invalid_client)
  end

  it "requires the token's client and current client to match" do
    subject.client = FactoryBot.create(:application)
    subject.validate
    expect(subject.error).to eq(:invalid_grant)
  end

  it "rejects revoked tokens" do
    refresh_token.revoke
    subject.validate
    expect(subject.error).to eq(:invalid_grant)
  end

  it "accepts expired tokens" do
    refresh_token.expires_in = -1
    refresh_token.save
    subject.validate
    expect(subject).to be_valid
  end

  context "refresh tokens expire on access token use" do
    let(:server) do
      double :server,
             access_token_expires_in: 2.minutes,
             custom_access_token_expires_in: lambda { |context|
               context.grant_type == Doorkeeper::OAuth::REFRESH_TOKEN ? 1234 : nil
             }
    end

    before do
      allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(true)
    end

    it "issues a new token for the client" do
      expect { subject.authorize }.to change { client.reload.access_tokens.count }.by(1)
    end

    it "does not revoke the previous token" do
      subject.authorize
      expect(refresh_token).not_to be_revoked
    end

    it "sets the previous refresh token in the new access token" do
      subject.authorize
      expect(
        # #sort_by used for MongoDB ORM extensions for valid ordering
        client.access_tokens.max_by(&:created_at).previous_refresh_token,
      ).to eq(refresh_token.refresh_token)
    end
  end

  context "clientless access tokens" do
    let!(:refresh_token) { FactoryBot.create(:clientless_access_token, use_refresh_token: true) }

    subject { described_class.new server, refresh_token, nil }

    it "issues a new token without a client" do
      expect { subject.authorize }.to change { Doorkeeper::AccessToken.count }.by(1)
    end
  end

  context "with scopes" do
    let(:refresh_token) do
      FactoryBot.create :access_token,
                        use_refresh_token: true,
                        scopes: "public write"
    end
    let(:parameters) { {} }

    subject { described_class.new server, refresh_token, credentials, parameters }

    it "transfers scopes from the old token to the new token" do
      subject.authorize
      expect(Doorkeeper::AccessToken.last.scopes).to eq(%i[public write])
    end

    it "reduces scopes to the provided scopes" do
      parameters[:scopes] = "public"
      subject.authorize
      expect(Doorkeeper::AccessToken.last.scopes).to eq(%i[public])
    end

    it "validates that scopes are included in the original access token" do
      parameters[:scopes] = "public update"

      subject.validate
      expect(subject.error).to eq(:invalid_scope)
    end

    it "uses params[:scope] in favor of scopes if present (valid)" do
      parameters[:scopes] = "public update"
      parameters[:scope] = "public"
      subject.authorize
      expect(Doorkeeper::AccessToken.last.scopes).to eq(%i[public])
    end

    it "uses params[:scope] in favor of scopes if present (invalid)" do
      parameters[:scopes] = "public"
      parameters[:scope] = "public update"

      subject.validate
      expect(subject.error).to eq(:invalid_scope)
    end
  end
end
