AnyEvent-MCP

 view release on metacpan or  search on metacpan

t/29_imap_oauth.t  view on Meta::CPAN

{
   no warnings "redefine";
   local *AnyEvent::HTTP::http_request = sub {
      my ($method, $endpoint, @args) = @_;
      my $cb = pop @args;
      my %args = @args;
      push @requests, [$method, $endpoint, \%args];
      my $body = $args{body};
      my $response = $body =~ /grant_type=authorization_code/
         ? '{"access_token":"first-access","refresh_token":"initial-refresh","token_type":"Bearer"}'
         : '{"access_token":"renewed-access","refresh_token":"rotated-refresh","token_type":"Bearer"}';
      $args{on_body}->($response, { Status => 200 });
      $cb->(undef, { Status => 200 });
      undef
   };

   my $granted = $oauth->can ("finish")->(\%kv, $state, $verifier, $redirect);
   is $granted->{access_token}, "first-access", "authorization code exchanged for access token";
   is $granted->{refresh_token}, "initial-refresh", "authorization code gets refresh token";
   is $requests[0][0], "POST", "token grant uses POST";
   is $requests[0][1], "https://login.example.org/token", "token grant uses configured endpoint";
   like $requests[0][2]{body}, qr/grant_type=authorization_code/, "authorization grant type";
   like $requests[0][2]{body}, qr/code_verifier=/, "PKCE verifier sent to token endpoint";
   like $requests[0][2]{body}, qr/client_secret=public-secret/, "client_secret_post configured";
   is $requests[0][2]{tls_ctx}, "high", "OAuth token HTTP uses verified TLS";
   is $requests[0][2]{recurse}, 0, "no redirect following for token endpoint";

   my $dir = tempdir (CLEANUP => 1);
   my $file = "$dir/account.ini";
   $kv{"oauth-refresh-token-base64"} = MIME::Base64::encode_base64 ("initial-refresh", "");
   my $lock = AnyEvent::MCP::File::lock $file;
   AnyEvent::MCP::File::replace $file, "[account]\n"
      . (join "", map "$_ = $kv{$_}\n", sort keys %kv);
   close $lock;

   is $oauth->can ("refresh")->($file, \%kv), "renewed-access", "refresh grant supplies access token";
   open my $fh, "<:raw", $file or die "$file: $!";
   my $saved = do { local $/; <$fh> };
   close $fh;
   like $saved, qr/oauth-refresh-token-base64 = /, "rotated refresh token persisted";
   unlike $saved, qr/initial-refresh|rotated-refresh|renewed-access/, "token values not persisted in cleartext";
   like $requests[1][2]{body}, qr/refresh_token=initial-refresh/, "refresh uses stored token";

   my $changed = { %kv, "oauth-client-id" => "other-client" };
   ok !eval { $oauth->can ("refresh")->($file, $changed); 1 },
      "reconnect refuses changed OAuth client registration";

   {
      local *AnyEvent::HTTP::http_request = sub {
         my ($method, $endpoint, @args) = @_;
         my $cb = pop @args;
         my %args = @args;
         push @requests, [$method, $endpoint, \%args];
         $args{on_body}->('{"access_token":"basic-access","token_type":"Bearer"}', { Status => 200 });
         $cb->(undef, { Status => 200 });
         undef
      };
      my %basic = (%kv, "oauth-client-auth" => "client_secret_basic");
      my $result = $oauth->can ("token")->(\%basic, grant_type => "refresh_token", refresh_token => "old");
      is $result->{access_token}, "basic-access", "client_secret_basic token response accepted";
      like $requests[-1][2]{headers}{Authorization}, qr/^Basic /, "basic client credentials sent in header";
      unlike $requests[-1][2]{body}, qr/client_secret/, "basic mode omits client secret from body";
   }

   {
      local *AnyEvent::HTTP::http_request = sub {
         my ($method, $endpoint, @args) = @_;
         my $cb = pop @args;
         my %args = @args;
         $args{on_body}->('{"error":"invalid_grant","error_description":"do not print secrets"}',
            { Status => 400 });
         $cb->(undef, { Status => 400 });
         undef
      };
      my $ok = eval { $oauth->can ("token")->(\%kv, grant_type => "refresh_token", refresh_token => "old"); 1 };
      ok !$ok, "failed token exchange rejects error response";
      like $@, qr/HTTP 400/, "token error identifies HTTP status";
      unlike $@, qr/do not print secrets/, "token error does not echo arbitrary provider error text";
   }

   {
      local *AnyEvent::HTTP::http_request = sub {
         my ($method, $endpoint, @args) = @_;
         my $cb = pop @args;
         my %args = @args;
         my $ok = $args{on_body}->("x" x 70000, { Status => 200 });
         $cb->(undef, { Status => $ok ? 200 : 598 });
         undef
      };
      my $ok = eval { $oauth->can ("token")->(\%kv, grant_type => "refresh_token", refresh_token => "old"); 1 };
      ok !$ok && $@ =~ /response too large/, "OAuth token response size is bounded";
   }
}

my $worker_tests = eval {
   require Mail::IMAPTalk;
   require IO::Socket::SSL;
   require Authen::SASL;
   require AnyEvent::MCP::Worker::IMAP;
   1
};
SKIP: {
   skip "optional IMAP worker dependencies not installed", 5 unless $worker_tests;

{
   package MockOAuthIMAP;
   our $sasl;
   sub new { bless {}, shift }
   sub capability { { "auth=xoauth2" => 1 } }
   sub authenticate {
      my ($self, $client) = @_;
      $sasl = $client->client_new ("imap", "imap.example.org")->client_start;
      1
   }
   sub uid { 1 }
   sub parse_mode { 1 }
   sub get_last_error { "fake auth error" }
}

{
   no warnings "redefine";



( run in 0.654 second using v1.01-cache-2.11-cpan-3f962061493 )