At

 view release on metacpan or  search on metacpan

t/08_resume.t  view on Meta::CPAN

use v5.42;
use Test2::V0;
use At;
use JSON::PP     qw[encode_json];
use MIME::Base64 qw[encode_base64];
use Crypt::PK::ECC;

# Build an expired-looking access JWT (payload is what resume() decodes; the
# signature is never verified here)
sub make_token {
    my (%claims) = @_;
    my $json     = encode_json( { sub => 'did:plc:test', iat => time - 3660, exp => time - 60, %claims } );
    my $b64      = encode_base64( $json, '' );
    $b64 =~ tr[+/][-_];
    $b64 =~ s/=+$//;
    "eyJhbGciOiJFUzI1NiJ9.$b64.fakesig";
}
my $key = Crypt::PK::ECC->new;
$key->generate_key('secp256r1');
my $jwk_private = $key->export_key_jwk('private');
subtest 'resume refreshes an expired DPoP access token even with an opaque refresh token' => sub {
    my $at   = At->new( host => 'bsky.social' );
    my $http = $at->http;
    my ( $set_access, $set_type, $set_key );
    my $http_mock = mock $http => (
        override => [
            post => sub {
                my ( $self, $url, $args ) = @_;
                is $url, 'https://as.example/token', 'token endpoint hit';
                return { access_token => 'new-access-token', refresh_token => 'new-refresh-token', sub => 'did:plc:test', scope => 'atproto' };
            },
            set_tokens => sub {
                my ( $self, $acc, $ref, $type, $key ) = @_;
                ( $set_access, $set_type, $set_key ) = ( $acc, $type, $key );
                return $self;
            }
        ]
    );
    my $at_mock = mock $at => (
        override => [
            oauth_discover => sub {
                return { pds => 'https://pds.example', metadata => { token_endpoint => 'https://as.example/token' } };
            },
        ]
    );
    my $access = make_token();
    my $ret
        = $at->resume( $access, 'opaque-refresh-token', 'DPoP', $jwk_private, 'http://localhost/client', 'atperl.bsky.social', 'https://pds.example',
        'atproto' );
    ok $ret, 'resume returns the refreshed user agent';
    is $set_access, 'new-access-token', 'set_tokens received the refreshed access token';
    is $set_type,   'DPoP',             'token type preserved';
    ok $set_key, 'dpop key re-bound';
    is $at->session->accessJwt,  'new-access-token',  'session holds the refreshed token';
    is $at->session->refreshJwt, 'new-refresh-token', 'session holds the refreshed refresh token';
};
subtest 'resume clears the session when refresh cannot proceed' => sub {
    my $at      = At->new( host => 'bsky.social' );
    my $at_mock = mock $at => (
        override => [
            oauth_discover => sub { return undef },

            # session() is lazy and falls back to a network fetch; keep it local
            get => sub { return undef }
        ]
    );
    my $ret
        = $at->resume( make_token(), 'opaque-refresh-token', 'DPoP', $jwk_private, 'cid', 'atperl.bsky.social', 'https://pds.example', 'atproto' );
    ok !$ret,                 'resume returns false when refresh cannot proceed';
    ok !defined $at->session, 'expired session not left in place';
};
subtest 'resume does not refresh a still-valid access token' => sub {
    my $at         = At->new( host => 'bsky.social' );
    my $http       = $at->http;
    my $refreshed  = 0;
    my $http_mock2 = mock $http => (
        override => [
            post => sub {
                $refreshed++;
                return { access_token => 'new-access-token', refresh_token => 'new-refresh-token', sub => 'did:plc:test', scope => 'atproto' };
            },
            set_tokens => sub ( $self, $acc, $ref, $type, $key ) {
                return $self;
            },
        ]
    );
    my $at_mock2 = mock $at => (
        override => [
            oauth_discover => sub {
                return { pds => 'https://pds.example', metadata => { token_endpoint => 'https://as.example/token' } };
            },
        ]
    );
    my $json = encode_json( { sub => 'did:plc:test', iat => time - 60, exp => time + 3600 } );
    my $b64  = encode_base64( $json, '' );
    $b64 =~ tr[+/][-_];
    $b64 =~ s/=+$//;
    my $fresh_access = "eyJhbGciOiJFUzI1NiJ9.$b64.fakesig";
    $at->resume( $fresh_access, 'opaque-refresh-token', 'DPoP', $jwk_private, undef, undef, undef, undef );
    is $refreshed,              0,             'no refresh attempted while access token is valid';
    is $at->session->accessJwt, $fresh_access, 'original access token kept';
};
#
done_testing;



( run in 0.390 second using v1.01-cache-2.11-cpan-16aabcbfbcb )