Ctrl K
ComunidadGuías…

test/integration/oauth_grant_idle_expiry_test.rb

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
require "test_helper"

# A grant lapses once its refresh token goes unused for the idle window, as
# bc3's refresh tokens do: each refresh restarts the clock, and a lapsed grant
# takes a fresh authorization, with consent.
class OauthGrantIdleExpiryTest < ActionDispatch::IntegrationTest
  setup do
    @client = oauth_clients(:mcp_client)
    freeze_time
    @grant = identities(:david).access_tokens.create!(oauth_client: @client, permission: :read)
  end

  test "a grant refreshes up to the end of the idle window" do
    later_by Identity::AccessToken::REFRESH_IDLE_LIMIT
    refresh @grant.raw_refresh_token

    assert_response :success
  end

  test "a grant idle past the window lapses" do
    later_by Identity::AccessToken::REFRESH_IDLE_LIMIT + 1.second
    refresh @grant.raw_refresh_token

    assert_response :bad_request
    assert_equal "invalid_grant", response.parsed_body["error"]
  end

  test "each refresh restarts the idle clock" do
    later_by Identity::AccessToken::REFRESH_IDLE_LIMIT - 1.day
    refresh @grant.raw_refresh_token
    assert_response :success

    later_by Identity::AccessToken::REFRESH_IDLE_LIMIT - 1.day
    refresh response.parsed_body["refresh_token"]

    assert_response :success
  end

  test "a lapsed grant leaves Connected Apps" do
    sign_in_as :david

    later_by Identity::AccessToken::REFRESH_IDLE_LIMIT + 1.second
    get my_connected_apps_path

    assert_response :success
    assert_no_match @client.name, response.body
  end

  test "the sweep removes lapsed grants and keeps live ones" do
    live = identities(:david).access_tokens.create!(oauth_client: @client, permission: :read)
    personal = identities(:david).access_tokens.create!(permission: :read, description: "PAT")

    later_by Identity::AccessToken::REFRESH_IDLE_LIMIT - 1.day
    live.refresh

    later_by 1.day + 1.second
    Identity::AccessToken.cleanup

    assert_not Identity::AccessToken.exists?(@grant.id)
    assert Identity::AccessToken.exists?(live.id)
    assert Identity::AccessToken.exists?(personal.id)
  end

  test "the sweep spares a grant renewed after it was selected" do
    later_by Identity::AccessToken::REFRESH_IDLE_LIMIT + 1.second
    # The sweep holds the grant as it was read when selected, before the renewal.
    selection = stub
    selection.stubs(:find_each).yields(Identity::AccessToken.find(@grant.id))
    Identity::AccessToken.stubs(:lapsed).returns(selection)

    Identity::AccessToken.where(id: @grant.id).update_all(updated_at: Time.current)
    Identity::AccessToken.cleanup

    assert Identity::AccessToken.exists?(@grant.id)
  end

  test "rotation itself refuses a lapsed grant, so one racing the deadline can't renew it" do
    later_by Identity::AccessToken::REFRESH_IDLE_LIMIT + 1.second
    presented = @grant.raw_refresh_token

    assert_nil @grant.refresh
    assert_equal TokenDigester.digest(presented), @grant.reload.refresh_token_digest
  end

  test "the idle clock is the grant's last rotation, so a rotation by older code mid-deploy restarts it too" do
    later_by Identity::AccessToken::REFRESH_IDLE_LIMIT - 1.day
    rotated_by_older_code = Identity::AccessToken.generate_unique_secure_token
    Identity::AccessToken.where(id: @grant.id).update_all(refresh_token_digest: TokenDigester.digest(rotated_by_older_code), updated_at: Time.current)

    later_by Identity::AccessToken::REFRESH_IDLE_LIMIT - 1.day
    refresh rotated_by_older_code

    assert_response :success
  end

  test "a lapsed grant takes its entry point off the profile page too" do
    sign_in_as :david

    get user_path(users(:david))
    assert_match my_connected_apps_path, response.body

    later_by Identity::AccessToken::REFRESH_IDLE_LIMIT + 1.second
    get user_path(users(:david))
    assert_no_match my_connected_apps_path, response.body
  end

  private
    # Elapsed time in the app's zone, as the model counts it: travel would add
    # calendar days in the local zone, an hour off across a DST change.
    def later_by(duration)
      travel_to Time.current + duration
    end

    def refresh(refresh_token)
      untenanted do
        post oauth_token_path, params: { grant_type: "refresh_token", refresh_token: refresh_token, client_id: @client.client_id }
      end
    end
end