Mojo-ATProto-OAuth

 view release on metacpan or  search on metacpan

t/resource_client.t  view on Meta::CPAN

use Test2::V0;
use feature 'signatures';
no warnings 'experimental::signatures';

# Same embedded-mock-app technique t/flow.t uses (no real socket - see
# t/par.t for why). Here the mock stands in for a user's own PDS, not
# an auth server - exercising Mojo::ATProto::OAuth::ResourceClient's own
# DPoP-nonce-rotation/access-token-refresh retry dance, not the OAuth
# handshake itself (already covered by flow.t).

use Mojo::ATProto::OAuth                       qw//;
use Mojo::ATProto::OAuth::DPoP                  qw//;
use Mojo::ATProto::OAuth::ResourceClient       qw//;
use Mojo::ATProto::OAuth::SessionStore::Memory qw//;
use Mojolicious::Lite;
use Mojo::UserAgent;

sub make_session ($store, %overrides) {
    my $dpop_key = Mojo::ATProto::OAuth::DPoP->generate_keypair;
    my $session  = {
        account_did                => 'did:plc:testuser',
        session_id                 => 'session-1',
        # Relative (no host) so Mojo::UserAgent resolves it against the
        # embedded test server via ->server->app - same technique
        # t/flow.t and t/par.t use for their own endpoint URLs, and for
        # the same reason (no real DNS/socket in this dev environment).
        host_url                   => '',
        auth_server_url            => 'https://auth.example.com',
        auth_server_token_endpoint => 'https://auth.example.com/token',
        scopes                     => ['atproto'],
        access_token               => 'access-1',
        refresh_token              => 'refresh-1',
        dpop_authserver_nonce      => 'authserver-nonce-1',
        dpop_host_nonce            => 'host-nonce-1',
        dpop_private_key_pem       => Mojo::ATProto::OAuth::DPoP->export_private_pem($dpop_key),
        %overrides,
    };
    $store->save_session($session);
    return $session;
}

sub make_oauth ($store, $ua) {
    return Mojo::ATProto::OAuth->new(
        client_id    => 'https://pib.example.com/oauth/client-metadata.json',
        callback_url => 'https://pib.example.com/oauth/callback',
        ua           => $ua,
        store        => $store,
    );
}

subtest '$oauth->client is a lazily-built, memoized ResourceClient wired to that oauth instance' => sub {
    my $store  = Mojo::ATProto::OAuth::SessionStore::Memory->new;
    my $oauth  = make_oauth($store, Mojo::UserAgent->new);
    my $client = $oauth->client;
    isa_ok($client, ['Mojo::ATProto::OAuth::ResourceClient']);
    ref_is($client->oauth, $oauth, 'wired to the oauth instance it was built from');
    ref_is($oauth->client, $client, 'the same instance is returned on subsequent calls, not rebuilt each time');
};

subtest 'plain authenticated GET, sync and async' => sub {
    my $seen_headers;
    my $app = Mojolicious::Lite->new;
    $app->routes->get('/xrpc/app.bsky.actor.getProfile' => sub ($c) {
        $seen_headers = {authorization => $c->req->headers->header('Authorization'), dpop => $c->req->headers->header('DPoP')};
        return $c->render(json => {did => 'did:plc:testuser', handle => 'alice.example.com'});
    });
    my $ua = Mojo::UserAgent->new;
    $ua->server->app($app);

    my $store  = Mojo::ATProto::OAuth::SessionStore::Memory->new;
    my $oauth  = make_oauth($store, $ua);
    make_session($store);
    my $client = Mojo::ATProto::OAuth::ResourceClient->new(oauth => $oauth);

    my $result = $client->request('did:plc:testuser', 'session-1', 'get', '/xrpc/app.bsky.actor.getProfile');
    is($result, {did => 'did:plc:testuser', handle => 'alice.example.com'}, 'decoded JSON body returned');
    is($seen_headers->{authorization}, 'DPoP access-1', 'access token sent as a DPoP-scheme Authorization header');
    ok(length($seen_headers->{dpop} // ''), 'a DPoP proof header was sent');

    my $result_p;
    $client->request_p('did:plc:testuser', 'session-1', 'get', '/xrpc/app.bsky.actor.getProfile')->then(sub ($r) { $result_p = $r })->wait;
    is($result_p, {did => 'did:plc:testuser', handle => 'alice.example.com'}, 'async counterpart returns the same decoded JSON body');
};

subtest 'POST with a JSON body' => sub {
    my $seen_body;
    my $app = Mojolicious::Lite->new;
    $app->routes->post('/xrpc/com.atproto.repo.putRecord' => sub ($c) {
        $seen_body = $c->req->json;
        return $c->render(json => {uri => 'at://did:plc:testuser/app.bsky.feed.post/abc', cid => 'bafycid'});
    });
    my $ua = Mojo::UserAgent->new;
    $ua->server->app($app);

    my $store  = Mojo::ATProto::OAuth::SessionStore::Memory->new;



( run in 0.450 second using v1.01-cache-2.11-cpan-8dfa8b56332 )