Dancer2-Session-Pg
view release on metacpan or search on metacpan
my $flipped = ( ord substr $altered, -1 ) ^ 0xFF;
substr $altered, -1, 1, chr $flipped;
T2->is( scalar $watched->_decrypt( SID, $altered ), undef, 'an altered payload does not decrypt' );
T2->is( scalar engine( key => q{b} x 64 )->_decrypt( SID, $blob ), undef, 'the wrong key yields nothing, not garbage' );
# Four distinct reasons, each reported once -- not once per request, which is
# how a log stops being readable when somebody is poking at cookies.
T2->is( scalar @reported, 4, 'each distinct reason is reported exactly once' );
T2->is( $reported[0][0], 'warning', 'at the level Dancer2 expects' );
T2->like( $reported[0][1], qr/could[ ]not[ ]be[ ]read/msx, 'through log_cb' );
$watched->_decrypt( SID, $altered ) for 1 .. 3;
T2->is( scalar @reported, 4, 'and repeating the same failure adds nothing to the log' );
}
);
T2->subtest_streamed(
'the principal, with no database in sight' => sub {
my $plain = engine();
T2->is( $plain->principal_column, 'principal_id', 'the column has a documented default' );
T2->is( scalar $plain->principal_key, undef, 'and principal_key is off by default' );
my $named = engine( principal_key => 'account_id', principal_column => 'account_id' );
T2->is( $named->_principal_of( { account_id => 4711 } ), 4711, 'a named key is copied' );
T2->is( scalar $named->_principal_of( {} ), undef, 'a missing one gives undef' );
T2->is( scalar $named->_principal_of( { account_id => { id => 1 } } ),
undef, 'and a reference is skipped -- only a scalar is worth indexing' );
my $computed = engine( principal_key => sub { return $_[0]->{'user'}{'id'} } );
T2->is( $computed->_principal_of( { user => { id => 'PRL-9' } } ),
'PRL-9', 'a coderef can reach an identity spread over several keys' );
T2->is( scalar $computed->_principal_of( { user => {} } ), undef, 'and gives undef when it finds nothing' );
}
);
# principal_key pointing at a structure leaves the column NULL, which makes
# destroy_for_principal unable to find the session -- an account suspension that
# silently does not take effect. It was silent; now it is reported, once, and
# WITHOUT the value, which is session content and the reason this module
# encrypts in the first place.
T2->subtest_streamed(
'a principal that is not a value is reported, not swallowed' => sub {
my @logged;
my $engine = Dancer2::Session::Pg->new(
dbh => sub { die "no\n" },
dbtable => 'sessions',
encryption_keys => { 0 => slot( $KEY32, undef, 1 ) },
principal_key => 'identity',
log_cb => sub { push @logged, [@_]; return 1 },
);
T2->is( scalar $engine->_principal_of( { identity => { id => 7, token => 'SECRET' } } ),
undef, 'a reference still yields no principal' );
T2->is( scalar @logged, 1, 'and it is reported' );
T2->is( $logged[0][0], 'warning', 'at warning level' );
T2->like( $logged[0][1], qr/destroy_for_principal/msx, 'naming the consequence rather than just the symptom' );
T2->like( $logged[0][1], qr/HASH[ ]reference/msx, 'and what it got' );
T2->ok( index( $logged[0][1], 'SECRET' ) < 0,
'but NOT the value -- that is session content, and some of it is credentials' );
$engine->_principal_of( { identity => { id => $_ } } ) for 1 .. 5;
T2->is( scalar @logged, 1, 'reported once per engine, not once per request' );
}
);
# The warning that a supplied handle is in manual-commit mode, which the module
# issues because a session write joining somebody else's transaction is real in
# the process and absent from the database until they commit. It needs no
# THE CODEREF FORM, which is the one the documentation recommends for
# Dancer2::Plugin::Database and DBIx::Class -- and which was silent, because
# BUILD cannot call a coderef (doing so would open a connection at construction,
# the very thing the coderef form avoids) and nothing checked it later. Same
# handle, same risk, no warning.
T2->subtest_streamed(
'manual-commit mode warns for a CODEREF handle too' => sub {
my $handle = bless { AutoCommit => 0, RaiseError => 1 }, 'Test::Handle';
my @warnings;
my @logged;
local $SIG{'__WARN__'} = sub { push @warnings, $_[0] };
my $engine = Dancer2::Session::Pg->new(
dbh => sub { $handle },
dbtable => 'sessions',
log_cb => sub { push @logged, "$_[0]: $_[1]" },
encryption_keys => { 0 => slot( $KEY32, undef, 1 ) },
);
T2->is( scalar @warnings, 0, 'nothing at construction -- the coderef is deliberately not called there' );
$engine->_dbh;
T2->is( scalar @warnings, 1, 'but the first access warns' );
T2->like( $warnings[0], qr/AutoCommit[ ]off/msx, 'saying what the state is' );
T2->is( scalar( grep { m/AutoCommit[ ]off/msx } @logged ), 1, 'and it reaches log_cb as well' );
# Once per worker, not once per request: this is on the path of every
# single session read and write.
$engine->_dbh for 1 .. 5;
T2->is( scalar @warnings, 1, 'and says it ONCE, however many requests follow' );
}
);
# PostgreSQL: the engine reads one attribute off the handle, so a blessed hashref
# is a sufficient stand-in and this can be checked anywhere.
T2->subtest_streamed(
'a supplied handle in manual-commit mode warns, once' => sub {
my @warnings;
local $SIG{'__WARN__'} = sub { push @warnings, $_[0] };
Dancer2::Session::Pg->new(
dbh => bless( { AutoCommit => 0 }, 'Test::Handle' ),
dbtable => 'sessions',
encryption_keys => { 0 => slot( $KEY32, undef, 1 ) },
);
T2->is( scalar @warnings, 1, 'AutoCommit off on a supplied handle warns' );
T2->like( $warnings[0], qr/AutoCommit[ ]off/msx, 'saying what the state is' );
T2->like( $warnings[0], qr/not[ ]durable[ ]until[ ]the[ ]caller[ ]commits/msx, 'and what it means for a session write' );
# BOTH routes, because they reach different places. carp reaches an engine
( run in 0.949 second using v1.01-cache-2.11-cpan-3f962061493 )