Catalyst-Plugin-OpenIDConnect
view release on metacpan or search on metacpan
t/04_store_redis.t view on Meta::CPAN
ok(
Catalyst::Plugin::OpenIDConnect::Utils::Store::Redis->DOES(
'Catalyst::Plugin::OpenIDConnect::Role::Store'
),
'Utils::Store::Redis consumes Role::Store',
);
# ---------------------------------------------------------------------------
# Instantiation
# ---------------------------------------------------------------------------
my $store = MockRedisStore->new(
prefix => 'test:oidc:code:',
code_ttl => 600,
);
ok( $store, 'Redis store created' );
is( $store->prefix, 'test:oidc:code:', 'prefix attribute set' );
is( $store->code_ttl, 600, 'code_ttl attribute set' );
# ---------------------------------------------------------------------------
# create_authorization_code
# ---------------------------------------------------------------------------
# The store receives a plain claims hashref (as extracted by the controller
# via get_user_claims before calling create_authorization_code).
my $user_claims = { sub => 'user-123', name => 'Test User', email => 't@example.com' };
my $code = $store->create_authorization_code(
'test-client',
$user_claims,
'openid profile email',
'http://localhost:3000/callback',
'nonce-abc',
);
ok( $code, 'create_authorization_code returns a code' );
like( $code, qr/^[a-zA-Z0-9]+$/, 'Code is alphanumeric' );
is( length($code), 128, 'Code is 128 characters long' );
# Verify that setex was called with the right key and TTL
my $calls = $store->_redis->recorded_calls;
is( scalar @$calls, 1, 'One Redis call made for create' );
is( $calls->[0][0], 'setex', 'setex was called' );
like( $calls->[0][1], qr/^test:oidc:code:/, 'Key has correct prefix' );
is( $calls->[0][2], 600, 'TTL is code_ttl' );
# ---------------------------------------------------------------------------
# get_authorization_code - field and user claims round-trip
# ---------------------------------------------------------------------------
my $data = $store->get_authorization_code($code);
ok( $data, 'get_authorization_code returns data' );
is( $data->{client_id}, 'test-client', 'client_id matches' );
is( $data->{scope}, 'openid profile email', 'scope matches' );
is( $data->{redirect_uri}, 'http://localhost:3000/callback', 'redirect_uri matches' );
is( $data->{nonce}, 'nonce-abc', 'nonce matches' );
ok( $data->{created_at}, 'created_at is set' );
ok( $data->{expires_at}, 'expires_at is set' );
# The user claims must survive the JSON round-trip intact so that the token
# endpoint can use them directly to build the ID/access tokens.
ok( ref($data->{user}) eq 'HASH',
'user claims are a plain hashref after Redis round-trip' );
is( $data->{user}{sub}, 'user-123', 'user.sub preserved through Redis' );
is( $data->{user}{name}, 'Test User', 'user.name preserved through Redis' );
is( $data->{user}{email}, 't@example.com', 'user.email preserved through Redis' );
is( $store->get_authorization_code('nonexistent'), undef,
'get_authorization_code returns undef for unknown code' );
# ---------------------------------------------------------------------------
# consume_authorization_code â atomic GETDEL, returns data (HIGH-4)
# ---------------------------------------------------------------------------
my $consumed_data = $store->consume_authorization_code($code);
ok( $consumed_data, 'consume_authorization_code returns the code data' );
is( $consumed_data->{client_id}, 'test-client', 'returned client_id matches' );
is( $consumed_data->{scope}, 'openid profile email', 'returned scope matches' );
is( $consumed_data->{redirect_uri}, 'http://localhost:3000/callback', 'returned redirect_uri matches' );
is( $consumed_data->{nonce}, 'nonce-abc', 'returned nonce matches' );
# Code must no longer be retrievable
is( $store->get_authorization_code($code), undef,
'Code is unavailable after consume' );
# Confirm getdel (not del) was called
my $getdel_calls = [ grep { $_->[0] eq 'getdel' } @{ $store->_redis->recorded_calls } ];
is( scalar @$getdel_calls, 1, 'getdel was called once' );
like( $getdel_calls->[0][1], qr/^test:oidc:code:/, 'getdel used correct key prefix' );
# Second consume must return undef (single-use)
is( $store->consume_authorization_code($code), undef,
'Second consume returns undef' );
# Double-consume must not die
lives_ok { $store->consume_authorization_code($code) }
'Consuming an already-consumed code does not die';
# ---------------------------------------------------------------------------
# CSPRNG: codes must be unique
# ---------------------------------------------------------------------------
{
my $s = MockRedisStore->new( prefix => 'u:' );
my %seen;
for ( 1 .. 20 ) {
my $c = $s->create_authorization_code(
'c', { sub => 'u1' }, 'openid', 'http://example.com/', undef );
ok( !$seen{$c}, "Code $_ is unique" );
$seen{$c}++;
}
}
# ---------------------------------------------------------------------------
# Configurable prefix and TTL
# ---------------------------------------------------------------------------
{
my $s2 = MockRedisStore->new(
prefix => 'myapp:auth:',
code_ttl => 300,
( run in 1.424 second using v1.01-cache-2.11-cpan-9581c071862 )