The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

use Test::More tests => 48;

use IO::File;
use IO::Prompt;
use Net::Radius::Packet;
use Net::Radius::Dictionary;
use Net::Radius::Server::Base qw/:match :set/;

# Init the dictionary for our test run...
BEGIN {
    my $fh = new IO::File "dict.$$", ">";
    print $fh <<EOF;
ATTRIBUTE	User-Name		1	string
ATTRIBUTE	User-Password		2	string
ATTRIBUTE	NAS-IP-Address		4	ipaddr
ATTRIBUTE	Reply-Message		18	string
EOF

    close $fh;
};

END { unlink 'dict.' . $$; }

unless ($ENV{NRS_INTERACTIVE})
{
    diag(<<EOF);


This test includes an interactive component. To enable it,
set the environment variable \$NRS_INTERACTIVE to some true
value.


EOF
}

use_ok('Net::Radius::Server::PAM');
diag("Using service 'login' for the remaining tests");

my $pam = Net::Radius::Server::PAM->new({ service => 'login' });
my $m_pam = $pam->fmatch();
my $s_pam = $pam->fset();
is(ref($m_pam), "CODE", "Match factory returns a coderef/sub");
is(ref($s_pam), "CODE", "Set factory returns a coderef/sub");

# Class hierarchy and contents
isa_ok($pam, 'Exporter');
isa_ok($pam, 'Class::Accessor');
isa_ok($pam, 'Net::Radius::Server');
isa_ok($pam, 'Net::Radius::Server::Set');
isa_ok($pam, 'Net::Radius::Server::Match');
isa_ok($pam, 'Net::Radius::Server::Set::Simple');
isa_ok($pam, 'Net::Radius::Server::PAM');

can_ok($pam, 'new');
can_ok($pam, 'log');
can_ok($pam, 'log_level');
can_ok($pam, 'fmatch');
can_ok($pam, 'result');
can_ok($pam, 'attr');		# Comes from ::Set::Simple
can_ok($pam, 'fset');
can_ok($pam, 'mk');		# Should croak() when called
can_ok($pam, '_set');
can_ok($pam, '_match');

like($pam->description, qr/Net::Radius::Server::PAM/, 
     "Description contains the class");
like($pam->description, qr/pam\.t/, "Description contains the filename");
like($pam->description, qr/:\d+\)$/, "Description contains the line");

# Create an incomplete Access-Request packet
my $d = new Net::Radius::Dictionary "dict.$$";
isa_ok($d, 'Net::Radius::Dictionary');
my $p = new Net::Radius::Packet $d;
isa_ok($p, 'Net::Radius::Packet');
$p->set_identifier(42);
$p->set_authenticator('1234567890abcdef');
$p->set_code("Access-Request");

my $hash = { dict => $d, secret => 'mysecret', request => $p };
is($m_pam->($hash), NRS_MATCH_FAIL, "Incomplete packet causes FAIL");

# Now we need to work with user-supplied input

sleep 1;
diag("\nFurther testing requires credentials to login to this box");
if ($ENV{NRS_INTERACTIVE} and prompt(q{Run this test? [y/n]: }, -yes))
{
    sleep 1;
    diag("\nWe need a username to test");
    my $login = getpwuid($<);
    my $user = prompt(qq{Username [$login]: }, -d => $login);

    sleep 1;
    diag("\nWe need the user's password to test authentication");
    my $pass = prompt(qq{Password for $user: }, -e => '*');

    # Create a working Access-Request packet and response
    $p->set_attr("User-Name" => $user);

    my $q = new Net::Radius::Packet $d;
    isa_ok($q, 'Net::Radius::Packet');
    $q->set_dict("dict.$$");
    
    # Now, test the correct password
    $p->set_password($pass, 'mysecret');

    $hash = { dict => $d, secret => 'mysecret', request => $p, 
	      response => $q };
    is($m_pam->($hash), NRS_MATCH_OK, "Correct password: Should match");

    # And the wrong password
    $p->set_password('bad' . $pass, 'mysecret');

    diag "A warning is ok here...";
    $hash = { dict => $d, secret => 'mysecret', request => $p, 
	      response => $q };
    is($m_pam->($hash), NRS_MATCH_FAIL, "Wrong password: Should fail");

    # Test the ->store_result attribute
    $pam->store_result('pass');
    $hash = { dict => $d, secret => 'mysecret', request => $p, 
	      response => $q };
    $p->set_password($pass, 'mysecret');
    is($m_pam->($hash), NRS_MATCH_OK, "Correct password: Should match");
    ok(exists $hash->{pass}, "Result properly stored");
    isa_ok($hash->{pass}, 'Authen::PAM');

    # When working with the set method alone, authentication should also
    # happen - Let's try it
    $pam->result(NRS_SET_RESPOND|NRS_SET_CONTINUE);
    $hash = { dict => $d, secret => 'mysecret', request => $p, 
	      response => $q };
    $p->set_password($pass, 'mysecret');
    is($s_pam->($hash), NRS_SET_RESPOND | NRS_SET_CONTINUE, 
       "Correct password in set: Should respond");
    ok(exists $hash->{pass}, "Result properly stored");
    isa_ok($hash->{pass}, 'Authen::PAM');

    # A set method that will fail
    $hash = { dict => $d, secret => 'mysecret', request => $p, 
	      response => $q };
    $p->set_password('bad' . $pass, 'mysecret');
    diag "A warning here is ok...";
    is($s_pam->($hash), NRS_SET_CONTINUE, 
       "Bad password in set: Should not respond");
    ok(! exists $hash->{pass}, "Result not stored");

    # Now work as a chain
    # When working with the set method alone, authentication should also
    # happen - Let's try it
    $hash = { dict => $d, secret => 'mysecret', request => $p, 
	      response => $q };
    $p->set_password($pass, 'mysecret');
    is($m_pam->($hash), NRS_MATCH_OK, "Correct password: Should match");
    ok(exists $hash->{pass}, "Result properly stored");
    isa_ok($hash->{pass}, 'Authen::PAM');

    # Add an environment var to be converted into a RADIUS attribute
    $hash->{pass}->pam_putenv('NAS-IP-Address=127.0.0.1');
    $hash->{pass}->pam_putenv('HOME=/dev/null');

    diag "Expect a few warnings below...";
    $pam->log_level(4);
    $pam->auto(1);
    $pam->attr([['Reply-Message' => 'Authenticated by PAM']]);

    is($s_pam->($hash), NRS_SET_RESPOND | NRS_SET_CONTINUE, 
       "Correct password in set: Should respond");
    ok(exists $hash->{pass}, "Result properly stored");
    isa_ok($hash->{pass}, 'Authen::PAM');
    is($hash->{response}->attr('NAS-IP-Address'), '127.0.0.1',
       "Conversion of env to RADIUS attributes");
    is($hash->{response}->attr('Reply-Message'), 'Authenticated by PAM',
       "Set RADIUS attributes via ->attr");
    is($hash->{response}->code, 'Access-Accept', 
       "Correct RADIUS response code");
    is($hash->{response}->identifier, $p->identifier, 
       "Correct RADIUS identifier");
    is($hash->{response}->authenticator, $p->authenticator, 
       "Correct RADIUS authenticator");
}
else
{
  SKIP: { skip "Interactive tests skipped or no credentials to test auth", 
	  22 };
}