Developer-Dashboard
view release on metacpan or search on metacpan
t/82-auth-coverage.t view on Meta::CPAN
#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Test::More;
use File::Spec;
use File::Temp qw(tempdir);
use Socket qw(AF_INET6);
use lib 'lib';
use Developer::Dashboard::PathRegistry;
use Developer::Dashboard::FileRegistry;
use Developer::Dashboard::Auth;
use Developer::Dashboard::JSON qw(json_encode);
# Hermetic runtime rooted in a throwaway HOME. The auth config layer resolves
# from the deepest .developer-dashboard directory found walking up from the cwd,
# so we must chdir into the temp home before building the registries.
my $home = tempdir( CLEANUP => 1 );
local $ENV{HOME} = $home;
chdir $home or die "Unable to chdir to $home: $!";
my $paths = Developer::Dashboard::PathRegistry->new( home => $home );
my $files = Developer::Dashboard::FileRegistry->new( paths => $paths );
my $auth = Developer::Dashboard::Auth->new( paths => $paths, files => $files );
isa_ok( $auth, 'Developer::Dashboard::Auth', 'constructed auth manager' );
# new() rejects missing registries -- exercises the die side of both ||-guards.
{
my $no_paths = eval { Developer::Dashboard::Auth->new(); 1 } ? '' : $@;
like( $no_paths, qr/Missing path registry/, 'new dies without a path registry' );
my $no_files = eval { Developer::Dashboard::Auth->new( paths => $paths ); 1 } ? '' : $@;
like( $no_files, qr/Missing file registry/, 'new dies without a file registry' );
}
# add_user success plus every guarded rejection path.
{
my $record = $auth->add_user( username => 'alice', password => 'password123' );
is( $record->{username}, 'alice', 'add_user stores the requested username' );
is( $record->{iterations}, 210_000, 'add_user records the default PBKDF2 work factor' );
my $no_username = eval { $auth->add_user( password => 'password123' ); 1 } ? '' : $@;
like( $no_username, qr/Missing username/, 'add_user dies without a username' );
my $no_password = eval { $auth->add_user( username => 'bob' ); 1 } ? '' : $@;
like( $no_password, qr/Missing password/, 'add_user dies without a password' );
my $bad_username = eval { $auth->add_user( username => 'bad name!', password => 'password123' ); 1 } ? '' : $@;
like( $bad_username, qr/unsupported characters/, 'add_user rejects unsupported username characters' );
my $short_password = eval { $auth->add_user( username => 'carol', password => 'short' ); 1 } ? '' : $@;
like( $short_password, qr/at least 8 characters/, 'add_user rejects passwords shorter than eight characters' );
# Force the write-failure die: make the target record path an existing
# directory so the ">:raw" open cannot succeed.
my $victim_file = $auth->_user_file('victim');
mkdir $victim_file or die "Unable to stage directory $victim_file: $!";
my $write_fail = eval { $auth->add_user( username => 'victim', password => 'password123' ); 1 } ? '' : $@;
like( $write_fail, qr/Unable to write/, 'add_user dies when the record file cannot be opened for writing' );
}
# verify_user guards, the PBKDF2 verification path, and a record that omits the
# iteration count (so the default work factor fills in).
{
ok( !defined $auth->verify_user(), 'verify_user returns undef without a username' );
ok( !defined $auth->verify_user( username => 'alice' ), 'verify_user returns undef without a password' );
ok( $auth->verify_user( username => 'alice', password => 'password123' ), 'verify_user accepts a valid PBKDF2 login' );
ok( !defined $auth->verify_user( username => 'alice', password => 'wrongpass1' ), 'verify_user rejects a wrong password' );
my $salt = 'coverage-salt';
my $hash = Developer::Dashboard::Auth::_pbkdf2_hmac_sha256_hex( 'password123', $salt, 210_000 );
my $file = $auth->_user_file('noiter');
open my $fh, '>', $file or die "Unable to write $file: $!";
print {$fh} json_encode(
{
username => 'noiter',
role => 'helper',
salt => $salt,
password_scheme => 'pbkdf2-hmac-sha256',
password_hash => $hash,
}
);
close $fh;
ok(
$auth->verify_user( username => 'noiter', password => 'password123' ),
'verify_user falls back to the default work factor when a record omits its iteration count',
);
}
# get_user must surface an open failure on a record that exists but is
# unreadable (running as a non-root owner, mode 0000 denies our own read).
{
my $locked = $auth->_user_file('locked');
open my $fh, '>', $locked or die "Unable to write $locked: $!";
print {$fh} json_encode( { username => 'locked' } );
close $fh;
chmod 0000, $locked;
my $read_fail = eval { $auth->get_user('locked'); 1 } ? '' : $@;
like( $read_fail, qr/Unable to read/, 'get_user dies when an existing record cannot be opened for reading' );
chmod 0600, $locked;
( run in 1.372 second using v1.01-cache-2.11-cpan-4ab04211f4c )