Files
webmin/postfix/t/run-tests.t
2026-05-23 17:15:31 -05:00

250 lines
10 KiB
Perl

#!/usr/bin/perl
use strict;
use warnings;
use Test::More;
use Cwd qw(abs_path);
use File::Temp qw(tempdir);
sub script_dir
{
my $path = $0;
if ($path =~ m{^/}) {
$path =~ s{/[^/]+$}{};
return $path;
}
my $cwd = `pwd`;
chomp($cwd);
if ($path =~ m{/}) {
$path =~ s{/[^/]+$}{};
return $cwd.'/'.$path;
}
return $cwd;
}
my $bindir = script_dir();
my $rootdir = abs_path("$bindir/../..") or die "rootdir: $!";
my $confdir = tempdir(CLEANUP => 1);
my $vardir = tempdir(CLEANUP => 1);
# Global Webmin config
open(my $cfh, ">", "$confdir/config") or die "config: $!";
print $cfh "os_type=linux\nos_version=0\n";
close($cfh);
open(my $vfh, ">", "$confdir/var-path") or die "var-path: $!";
print $vfh "$vardir\n";
close($vfh);
# A main.cf to drive the config parser
my $maincf = "$confdir/main.cf";
my $mastercf = "$confdir/master.cf";
open(my $mc, ">", $maincf) or die "main.cf: $!";
print $mc <<'EOF';
myhostname = mail.example.com
mydestination = example.com,
mail.example.com,
localhost
compatibility_level = 2
util_lt = {{$compatibility_level} < {3} ? {old} : {new}}
util_ge = {{$compatibility_level} >= {3} ? {high} : {low}}
util_eq = {{$compatibility_level} == {2} ? {match} : {nomatch}}
subtest = key1 valueA key2 valueB
alias_maps = hash:/etc/postfix/aliases, hash:/etc/aliases
EOF
close($mc);
# A master.cf with one enabled and one disabled service, plus a continuation
open(my $ms, ">", $mastercf) or die "master.cf: $!";
print $ms <<'EOF';
smtp inet n - y - - smtpd
#qmgr unix n - n 300 1 qmgr
pickup unix n - y 60 1 pickup
-o content_filter=foo
EOF
close($ms);
# Per-module config
mkdir "$confdir/postfix" or die "postfix confdir: $!";
open(my $mod, ">", "$confdir/postfix/config") or die "module config: $!";
print $mod "postfix_config_file=$maincf\n";
print $mod "postfix_master=$mastercf\n";
print $mod "postfix_config_command=/bin/true\n";
print $mod "postfix_control_command=/bin/true\n";
print $mod "prefix_cmts=0\n";
close($mod);
# Pre-seed the version file so loading the lib does not shell out to postconf.
open(my $ver, ">", "$confdir/postfix/version") or die "version: $!";
print $ver "3.4.0\n";
close($ver);
$ENV{'WEBMIN_CONFIG'} = $confdir;
$ENV{'WEBMIN_VAR'} = $vardir;
$ENV{'FOREIGN_MODULE_NAME'} = 'postfix';
$ENV{'FOREIGN_ROOT_DIRECTORY'} = $rootdir;
chdir("$bindir/..") or die "chdir: $!";
require "$bindir/../postfix-lib.pl";
our (%config, $postfix_version, $virtual_maps, $ldap_timeout, $config_dir);
# --- load-time globals -----------------------------------------------------
is($postfix_version, '3.4.0', 'postfix_version read from version file');
is($virtual_maps, 'virtual_alias_maps', 'virtual_maps for >= 2.x');
is($ldap_timeout, 'ldap_timeout', 'ldap_timeout for >= 2.x');
is($config_dir, $confdir, 'guess_config_dir is dirname of main.cf');
# --- file_map_type ---------------------------------------------------------
ok(file_map_type('hash'), 'hash is a file map type');
ok(file_map_type('pcre'), 'pcre is a file map type');
ok(file_map_type('cidr'), 'cidr is a file map type');
ok(!file_map_type('mysql'),'mysql is not a file map type');
ok(!file_map_type('ldap'), 'ldap is not a file map type');
# --- get_maps_types_files --------------------------------------------------
is_deeply([ get_maps_types_files('hash:/etc/postfix/canonical') ],
[ [ 'hash', '/etc/postfix/canonical' ] ],
'single type:file parsed');
is_deeply([ get_maps_types_files(
'hash:/etc/postfix/canonical, proxy:pcre:/etc/postfix/x') ],
[ [ 'hash', '/etc/postfix/canonical' ],
[ 'pcre', '/etc/postfix/x' ] ],
'multiple maps and proxy: prefix parsed');
is_deeply([ get_maps_types_files('mysql:/etc/postfix/m.cf') ],
[ [ 'mysql', '/etc/postfix/m.cf' ] ],
'mysql backend type:file parsed');
is_deeply([ get_maps_types_files('') ], [],
'empty value yields no maps');
is_deeply([ get_maps_types_files('garbage-without-colon') ], [],
'unparseable value yields no maps');
# --- get_maps_files (path extraction) --------------------------------------
is_deeply([ get_maps_files('hash:/etc/postfix/aliases,hash:/etc/aliases') ],
[ '/etc/postfix/aliases', '/etc/aliases' ],
'get_maps_files extracts both file paths');
is_deeply([ get_maps_files('static:foo') ], [],
'get_maps_files ignores non-path map values');
# --- get_current_value (main.cf parser) ------------------------------------
is(get_current_value('myhostname', 1), 'mail.example.com',
'single-line parameter parsed');
my $dest = get_current_value('mydestination', 1);
like($dest, qr/example\.com/, 'continuation line: first value present');
like($dest, qr/localhost/, 'continuation line: trailing value joined');
is(get_current_value('no_such_param', 1), undef,
'unknown parameter with nodef returns undef (no postconf fallback)');
is(get_current_value('subtest:key1', 1), 'valueA',
'sub-parameter extraction (foo:bar) returns value after the key');
# --- resolve_current_value (compatibility_level conditionals) --------------
# Exercises the operator-dispatch rewrite of the old stringy eval.
is(resolve_current_value('util_lt'), 'old',
'resolve "<": level 2 is < 3, takes true branch');
is(resolve_current_value('util_ge'), 'low',
'resolve ">=": level 2 is not >= 3, takes false branch');
is(resolve_current_value('util_eq'), 'match',
'resolve "==": level 2 == 2, takes true branch');
is(resolve_current_value('myhostname'), 'mail.example.com',
'resolve passes through a plain value unchanged');
# --- master.cf parser + serializer -----------------------------------------
my $master = get_master_config();
is(ref($master), 'ARRAY', 'get_master_config returns array ref');
is(scalar(@$master), 3, 'three service entries parsed');
my ($smtp) = grep { $_->{'name'} eq 'smtp' } @$master;
my ($qmgr) = grep { $_->{'name'} eq 'qmgr' } @$master;
my ($pickup) = grep { $_->{'name'} eq 'pickup' } @$master;
ok($smtp, 'smtp service parsed');
ok($smtp->{'enabled'}, 'smtp is enabled');
is($smtp->{'type'}, 'inet', 'smtp type');
is($smtp->{'chroot'}, 'y', 'smtp chroot column');
is($smtp->{'command'}, 'smtpd','smtp command');
ok($qmgr, 'commented service still parsed');
ok(!$qmgr->{'enabled'}, 'commented service is disabled');
ok($pickup, 'pickup service parsed');
is($pickup->{'command'}, 'pickup -o content_filter=foo',
'continuation line appended to command');
# master_line round-trips an entry back to its on-disk form
is(master_line($smtp),
"smtp\tinet\tn\t-\ty\t-\t-\tsmtpd",
'master_line serializes an enabled service with tabs');
is(master_line($qmgr),
"#qmgr\tunix\tn\t-\tn\t300\t1\tqmgr",
'master_line prefixes a disabled service with #');
# --- is_table_comment / make_table_comment ---------------------------------
{
local $config{'prefix_cmts'} = 0;
is(is_table_comment('# hello world'), 'hello world',
'plain comment text extracted when prefix_cmts off');
is_deeply([ make_table_comment('a note') ], [ '# a note' ],
'make_table_comment emits a plain # line');
is_deeply([ make_table_comment('') ], [],
'make_table_comment emits nothing for empty comment');
}
{
local $config{'prefix_cmts'} = 1;
is(is_table_comment('# Webmin: tagged'), 'tagged',
'Webmin-tagged comment extracted when prefix_cmts on');
is(is_table_comment('# untagged'), undef,
'untagged comment ignored when prefix_cmts on');
is_deeply([ make_table_comment('z') ], [ '# Webmin: z' ],
'make_table_comment emits a Webmin-tagged line when prefix_cmts on');
}
# --- in_props --------------------------------------------------------------
is(in_props([ qw(cn foo objectClass top) ], 'objectClass'), 'top',
'in_props returns value following a matched name');
is(in_props([ qw(cn foo) ], 'mail'), undef,
'in_props returns undef for an absent name');
is(in_props([ qw(CN foo) ], 'cn'), 'foo',
'in_props matches case-insensitively');
# --- get_ldap_key ----------------------------------------------------------
is_deeply([ get_ldap_key({}) ],
[ 'mailacceptinggeneralid', '(mailacceptinggeneralid=*)' ],
'get_ldap_key default attribute and filter');
is_deeply([ get_ldap_key({ 'query_filter' => 'mail=%s' }) ],
[ 'mail', '(mail=*)' ],
'get_ldap_key derives attribute and filter from query_filter');
# --- make_map_ldap_dn ------------------------------------------------------
{
local $config{'ldap_doms'} = 1; # allow sub-domain DNs
local $config{'ldap_id'} = undef; # default to cn
my $conf = { 'search_base' => 'dc=example,dc=com', 'scope' => 'sub' };
is(make_map_ldap_dn({ 'name' => 'user@dom.com' }, $conf),
'cn=user,cn=dom.com,dc=example,dc=com',
'make_map_ldap_dn builds a per-user DN inside a domain');
is(make_map_ldap_dn({ 'name' => '@dom.com' }, $conf),
'cn=default,cn=dom.com,dc=example,dc=com',
'make_map_ldap_dn builds a catch-all DN for a domain');
is(make_map_ldap_dn({ 'name' => 'literal' }, $conf),
'cn=literal,dc=example,dc=com',
'make_map_ldap_dn builds a flat DN for a non-address name');
}
# --- get_backend_config ----------------------------------------------------
my $backend = "$confdir/mysql.cf";
open(my $bfh, ">", $backend) or die "backend: $!";
print $bfh "user = postfix\npassword = secret\n# a comment\nhosts = localhost\n";
close($bfh);
my $bc = get_backend_config($backend);
is($bc->{'user'}, 'postfix', 'backend config user parsed');
is($bc->{'password'}, 'secret', 'backend config password parsed');
is($bc->{'hosts'}, 'localhost', 'backend config hosts parsed');
# --- list_smtpd_restrictions (version-dependent constant) ------------------
my @restr = list_smtpd_restrictions();
ok((grep { $_ eq 'permit_mynetworks' } @restr),
'smtpd restrictions include permit_mynetworks');
ok((grep { $_ eq 'reject_unknown_reverse_client_hostname' } @restr),
'>= 2.3 uses reject_unknown_reverse_client_hostname');
done_testing();