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 )