Add Qpsmtpd::DB testing and more greylist testing
This commit is contained in:
parent
fa2ca922c5
commit
11646b9a27
2
.gitignore
vendored
2
.gitignore
vendored
@ -17,6 +17,8 @@ denysoft_greylist.dbm
|
|||||||
denysoft_greylist.dbm.lock
|
denysoft_greylist.dbm.lock
|
||||||
greylist.dbm
|
greylist.dbm
|
||||||
greylist.dbm.lock
|
greylist.dbm.lock
|
||||||
|
greylisting.dbm
|
||||||
|
greylisting.dbm.lock
|
||||||
|
|
||||||
/cover_db/
|
/cover_db/
|
||||||
.last_cover_stats
|
.last_cover_stats
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
package Qpsmtpd::DB;
|
package Qpsmtpd::DB;
|
||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings;
|
||||||
use Qpsmtpd::DB::File::DBM;
|
|
||||||
use Qpsmtpd::DB::Redis;
|
our @child_classes = qw(
|
||||||
|
Qpsmtpd::DB::Redis
|
||||||
|
Qpsmtpd::DB::File::DBM
|
||||||
|
);
|
||||||
|
|
||||||
sub new {
|
sub new {
|
||||||
my ( $class, %arg ) = @_;
|
my ( $class, %arg ) = @_;
|
||||||
# Qpsmtpd::DB::File::DBM is the only supported class just now
|
my @try_classes = @child_classes;
|
||||||
my @child_classes = qw(
|
if ( my $manual_class = delete $arg{class} ) {
|
||||||
Qpsmtpd::DB::Redis
|
@try_classes = ( $manual_class );
|
||||||
Qpsmtpd::DB::File::DBM
|
}
|
||||||
);
|
|
||||||
my $manual_class = delete $arg{class};
|
|
||||||
return $manual_class->new(%arg) if $manual_class;
|
|
||||||
my ( $child, @errors );
|
my ( $child, @errors );
|
||||||
for my $child_class ( @child_classes ) {
|
for my $child_class ( @try_classes ) {
|
||||||
|
eval "use $child_class";
|
||||||
eval {
|
eval {
|
||||||
$child = $child_class->new(%arg);
|
$child = $child_class->new(%arg);
|
||||||
};
|
};
|
||||||
|
@ -3,11 +3,6 @@ use strict;
|
|||||||
use warnings;
|
use warnings;
|
||||||
use parent 'Qpsmtpd::DB';
|
use parent 'Qpsmtpd::DB';
|
||||||
|
|
||||||
sub new {
|
|
||||||
my ( $class, %arg ) = @_;
|
|
||||||
return bless { %arg }, $class;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub dir {
|
sub dir {
|
||||||
my ( $self, @candidate_dirs ) = @_;
|
my ( $self, @candidate_dirs ) = @_;
|
||||||
return $self->{dir} if $self->{dir} and ! @candidate_dirs;
|
return $self->{dir} if $self->{dir} and ! @candidate_dirs;
|
||||||
|
@ -8,6 +8,11 @@ BEGIN { @AnyDBM_File::ISA = qw(DB_File GDBM_File NDBM_File) }
|
|||||||
use AnyDBM_File;
|
use AnyDBM_File;
|
||||||
use Fcntl qw(:DEFAULT :flock LOCK_EX LOCK_NB);
|
use Fcntl qw(:DEFAULT :flock LOCK_EX LOCK_NB);
|
||||||
|
|
||||||
|
sub new {
|
||||||
|
my ( $class, %arg ) = @_;
|
||||||
|
return bless {%arg}, $class;
|
||||||
|
}
|
||||||
|
|
||||||
sub file_extension {
|
sub file_extension {
|
||||||
my ( $self, $extension ) = @_;
|
my ( $self, $extension ) = @_;
|
||||||
return $self->{file_extension} ||= '.dbm';
|
return $self->{file_extension} ||= '.dbm';
|
||||||
@ -152,4 +157,14 @@ sub delete {
|
|||||||
delete $tied->{$key};
|
delete $tied->{$key};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub flush {
|
||||||
|
my ( $self ) = @_;
|
||||||
|
my $tied = $self->{tied};
|
||||||
|
if ( ! $tied ) {
|
||||||
|
warn "DBM db not yet set up, flush() failed\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete $tied->{$_} for keys %$tied;
|
||||||
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
@ -107,6 +107,8 @@ sub delete {
|
|||||||
return $self->redis->del($key);
|
return $self->redis->del($key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub flush { shift->redis->flushdb }
|
||||||
|
|
||||||
package MyRedis;
|
package MyRedis;
|
||||||
eval "use parent 'Redis'";
|
eval "use parent 'Redis'";
|
||||||
|
|
||||||
|
@ -8,11 +8,6 @@ use Qpsmtpd::Constants;
|
|||||||
|
|
||||||
my $test_email = 'user@example.com';
|
my $test_email = 'user@example.com';
|
||||||
|
|
||||||
my @greydbs = qw( denysoft_greylist.dbm denysoft_greylist.dbm.lock );
|
|
||||||
foreach ( @greydbs ) {
|
|
||||||
unlink $_ if -f $_;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub register_tests {
|
sub register_tests {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
|
|
||||||
@ -238,24 +233,38 @@ sub test_greylist {
|
|||||||
$self->connection->remote_ip('1.2.3.4');
|
$self->connection->remote_ip('1.2.3.4');
|
||||||
my $sender = Qpsmtpd::Address->new( "<$test_email>" );
|
my $sender = Qpsmtpd::Address->new( "<$test_email>" );
|
||||||
my $rcpt = Qpsmtpd::Address->new( "<$test_email>" );
|
my $rcpt = Qpsmtpd::Address->new( "<$test_email>" );
|
||||||
my $start = time() - 40 * 3600 * 24; # 40 days ago
|
for my $test_class (@Qpsmtpd::DB::child_classes) {
|
||||||
$mocktime = $start;
|
delete $self->{db};
|
||||||
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
|
eval { $self->db( class => $test_class ) };
|
||||||
'DENYSOFT: This mail is temporarily denied',
|
if ( $@ ) {
|
||||||
'Initial connection attempt greylisted' );
|
warn "Unable to test greylisting against $test_class: $@";
|
||||||
$mocktime = $start + 60 * 49;
|
next;
|
||||||
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
|
}
|
||||||
'DENYSOFT: This mail is temporarily denied',
|
$self->db->lock;
|
||||||
'Greylisted 49 minutes later' );
|
$self->db->flush;
|
||||||
$mocktime = $start + 60 * 51;
|
$self->db->unlock;
|
||||||
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
|
my $start = time() - 40 * 3600 * 24; # 40 days ago
|
||||||
'DECLINED',
|
$mocktime = $start;
|
||||||
'Allowed 51 minutes later' );
|
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
|
||||||
$mocktime = $start + 60 * 52 + 36 * 3600 * 24;
|
'DENYSOFT: This mail is temporarily denied',
|
||||||
$self->prune_db;
|
'Initial connection attempt greylisted' );
|
||||||
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
|
$mocktime = $start + 60 * 49;
|
||||||
'DENYSOFT: This mail is temporarily denied',
|
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
|
||||||
're-greylisted 36 days later' );
|
'DENYSOFT: This mail is temporarily denied',
|
||||||
|
'Greylisted 49 minutes later' );
|
||||||
|
$mocktime = $start + 60 * 51;
|
||||||
|
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
|
||||||
|
'DECLINED',
|
||||||
|
'Allowed 51 minutes later' );
|
||||||
|
$mocktime = $start + 60 * 52 + 36 * 3600 * 24;
|
||||||
|
$self->prune_db;
|
||||||
|
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
|
||||||
|
'DENYSOFT: This mail is temporarily denied',
|
||||||
|
're-greylisted 36 days later' );
|
||||||
|
$self->db->lock;
|
||||||
|
$self->db->flush;
|
||||||
|
$self->db->unlock;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
47
t/qpsmtpd-db-file.t
Normal file
47
t/qpsmtpd-db-file.t
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Test::More;
|
||||||
|
|
||||||
|
use lib 'lib'; # test lib/Qpsmtpd (vs site_perl)
|
||||||
|
use lib 't';
|
||||||
|
|
||||||
|
use_ok('Qpsmtpd::DB::File');
|
||||||
|
|
||||||
|
__qphome();
|
||||||
|
__validate_dir();
|
||||||
|
__dir();
|
||||||
|
|
||||||
|
done_testing();
|
||||||
|
|
||||||
|
sub __qphome {
|
||||||
|
my $db = FakeDB->new;
|
||||||
|
is( $db->qphome, 't', 'qphome()' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub __validate_dir {
|
||||||
|
my $db = FakeDB->new;
|
||||||
|
is( $db->validate_dir(), 0, 'validate_dir(): false on no input' );
|
||||||
|
is( $db->validate_dir(undef), 0, 'validate_dir(): false on undef' );
|
||||||
|
is( $db->validate_dir('invalid'), 0,
|
||||||
|
'validate_dir(): false for non-existent directory' );
|
||||||
|
is( $db->validate_dir('t/config'), 1,
|
||||||
|
'validate_dir(): true for real directory' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub __dir {
|
||||||
|
my $db = FakeDB->new;
|
||||||
|
is( $db->dir(), 't/config', 'default directory' );
|
||||||
|
is( $db->dir('_invalid','t/Test'), 't/Test', 'skip invalid candidate dirs' );
|
||||||
|
$db->{dir} = '_cached';
|
||||||
|
is( $db->dir(), '_cached', 'cached directory' );
|
||||||
|
is( $db->dir('t/Test'), 't/Test', 'passing candidate dirs resets cache' );
|
||||||
|
is( $db->dir('_invalid'), 't/config', 'invalid candidate dirs reverts to default' );
|
||||||
|
}
|
||||||
|
|
||||||
|
package FakeDB;
|
||||||
|
use parent 'Qpsmtpd::DB::File';
|
||||||
|
sub new {
|
||||||
|
my $class = shift;
|
||||||
|
return bless {@_}, $class;
|
||||||
|
}
|
68
t/qpsmtpd-db.t
Normal file
68
t/qpsmtpd-db.t
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Test::More;
|
||||||
|
|
||||||
|
use lib 'lib'; # test lib/Qpsmtpd (vs site_perl)
|
||||||
|
use lib 't';
|
||||||
|
|
||||||
|
use_ok('Qpsmtpd::DB');
|
||||||
|
use_ok('Qpsmtpd');
|
||||||
|
|
||||||
|
__new();
|
||||||
|
__lock();
|
||||||
|
__unlock();
|
||||||
|
__name();
|
||||||
|
|
||||||
|
done_testing();
|
||||||
|
|
||||||
|
sub __new {
|
||||||
|
@Qpsmtpd::DB::child_classes = qw(
|
||||||
|
BrokenClassOne
|
||||||
|
BrokenClassTwo
|
||||||
|
);
|
||||||
|
my $db;
|
||||||
|
eval { $db = Qpsmtpd::DB->new };
|
||||||
|
is( $@, "Couldn't load any storage modules\n"
|
||||||
|
. "Couldn't load BrokenClassOne: fool me once, shame on me\n\n"
|
||||||
|
. "Couldn't load BrokenClassTwo: fool me can't get fooled again\n",
|
||||||
|
'Detect failure to load all child DB classes' );
|
||||||
|
eval { $db = Qpsmtpd::DB->new( class => 'BrokenClassOne' ) };
|
||||||
|
is( $@, "fool me once, shame on me\n",
|
||||||
|
'Failure to load manual class' );
|
||||||
|
@Qpsmtpd::DB::child_classes = qw( EmptyClass );
|
||||||
|
eval { $db = Qpsmtpd::DB->new };
|
||||||
|
is( ref $db, 'EmptyClass',
|
||||||
|
'Load object with manual (bogus) class: Qpsmtpd object is returned' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub __lock {
|
||||||
|
@Qpsmtpd::DB::child_classes = qw( EmptyClass );
|
||||||
|
is( Qpsmtpd::DB->new->lock, 1, 'Default lock() method just returns true' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub __unlock {
|
||||||
|
@Qpsmtpd::DB::child_classes = qw( EmptyClass );
|
||||||
|
is( Qpsmtpd::DB->new->unlock, 1, 'Default lock() method just returns true' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub __name {
|
||||||
|
@Qpsmtpd::DB::child_classes = qw( EmptyClass );
|
||||||
|
my $db = Qpsmtpd::DB->new;
|
||||||
|
is( $db->name, undef, 'no name set yet' );
|
||||||
|
is( $db->name('test'), 'test', 'set name' );
|
||||||
|
is( $db->name, 'test', 'get name' );
|
||||||
|
}
|
||||||
|
|
||||||
|
package BrokenClassOne;
|
||||||
|
sub new { die "fool me once, shame on me\n" }
|
||||||
|
|
||||||
|
package BrokenClassTwo;
|
||||||
|
sub new { die "fool me can't get fooled again\n" }
|
||||||
|
|
||||||
|
package EmptyClass;
|
||||||
|
use parent 'Qpsmtpd::DB';
|
||||||
|
sub new {
|
||||||
|
my $class = shift;
|
||||||
|
return bless {@_}, $class;
|
||||||
|
}
|
34
t/qpsmtpd-plugin.t
Normal file
34
t/qpsmtpd-plugin.t
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Test::More;
|
||||||
|
|
||||||
|
use lib 'lib'; # test lib/Qpsmtpd (vs site_perl)
|
||||||
|
use lib 't';
|
||||||
|
|
||||||
|
use_ok('Qpsmtpd::Plugin');
|
||||||
|
|
||||||
|
__db();
|
||||||
|
|
||||||
|
done_testing();
|
||||||
|
|
||||||
|
sub __db {
|
||||||
|
my $plugin = FakePlugin->new;
|
||||||
|
my $db = $plugin->db( class => 'FakeDB', name => 'testfoo' );
|
||||||
|
is( ref $db, 'FakeDB', 'Qpsmtpd::Plugin::db(): Returns DB object' );
|
||||||
|
is( ref $plugin->{db}, 'FakeDB', 'DB object is cached' );
|
||||||
|
is( $db->{name}, 'testfoo', 'accepts name argument' );
|
||||||
|
delete $plugin->{db};
|
||||||
|
$db = $plugin->db( class => 'FakeDB' );
|
||||||
|
is( $db->{name}, 'testbar', 'name argument defaults to plugin name' );
|
||||||
|
}
|
||||||
|
|
||||||
|
package FakePlugin;
|
||||||
|
use parent 'Qpsmtpd::Plugin';
|
||||||
|
sub plugin_name { 'testbar' }
|
||||||
|
|
||||||
|
package FakeDB;
|
||||||
|
sub new {
|
||||||
|
my $class = shift;
|
||||||
|
return bless {@_}, $class;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user