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 )