summaryrefslogtreecommitdiffstats
path: root/t
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 17:29:54 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 17:29:54 +0000
commit618e47799afdfc2783d8469ca909aafa4acfa7b6 (patch)
tree6e12471be3cad7fb33c7f1b427b431bdf7dcf28d /t
parentInitial commit. (diff)
downloadinit-system-helpers-618e47799afdfc2783d8469ca909aafa4acfa7b6.tar.xz
init-system-helpers-618e47799afdfc2783d8469ca909aafa4acfa7b6.zip
Adding upstream version 1.66.upstream/1.66upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 't')
-rw-r--r--t/001-deb-systemd-helper.t473
-rw-r--r--t/002-deb-systemd-helper-update.t196
-rw-r--r--t/003-deb-systemd-helper-complex.t123
-rw-r--r--t/004-deb-systemd-helper-user.t413
-rw-r--r--t/README15
-rw-r--r--t/helpers.pm160
6 files changed, 1380 insertions, 0 deletions
diff --git a/t/001-deb-systemd-helper.t b/t/001-deb-systemd-helper.t
new file mode 100644
index 0000000..c78ad53
--- /dev/null
+++ b/t/001-deb-systemd-helper.t
@@ -0,0 +1,473 @@
+#!perl
+# vim:ts=4:sw=4:et
+
+use strict;
+use warnings;
+use Test::More;
+use File::Temp qw(tempfile tempdir); # in core since perl 5.6.1
+use File::Path qw(make_path); # in core since Perl 5.001
+use File::Basename; # in core since Perl 5
+use FindBin; # in core since Perl 5.00307
+
+use lib "$FindBin::Bin/.";
+use helpers;
+
+test_setup();
+
+my $dpkg_root = $ENV{DPKG_ROOT} // '';
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “is-enabled” is not true for a random, non-existing unit file. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+my ($fh, $random_unit) = tempfile('unit\x2dXXXXX',
+ SUFFIX => '.service',
+ TMPDIR => 1,
+ UNLINK => 1);
+close($fh);
+$random_unit = basename($random_unit);
+
+isnt_enabled($random_unit);
+isnt_debian_installed($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “is-enabled” is not true for a random, existing unit file. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+my $servicefile_path = "$dpkg_root/lib/systemd/system/$random_unit";
+make_path("$dpkg_root/lib/systemd/system");
+open($fh, '>', $servicefile_path);
+print $fh <<'EOT';
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+WantedBy=multi-user.target
+EOT
+close($fh);
+
+isnt_enabled($random_unit);
+isnt_debian_installed($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “enable” creates the requested symlinks. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+unless ($ENV{'TEST_ON_REAL_SYSTEM'}) {
+ # This might exist if we don't start from a fresh directory
+ ok(! -d "$dpkg_root/etc/systemd/system/multi-user.target.wants",
+ 'multi-user.target.wants does not exist yet');
+}
+
+my $retval = dsh('enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+my $symlink_path = "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit";
+ok(-l $symlink_path, "$random_unit was enabled");
+is($dpkg_root . readlink($symlink_path), $servicefile_path,
+ "symlink points to $servicefile_path");
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “is-enabled” now returns true. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+is_enabled($random_unit);
+is_debian_installed($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify deleting the symlinks and running “enable” again does not ┃
+# ┃ re-create the symlinks. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+unlink($symlink_path);
+ok(! -l $symlink_path, 'symlink deleted');
+isnt_enabled($random_unit);
+is_debian_installed($random_unit);
+
+$retval = dsh('enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+
+isnt_enabled($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “disable” when purging deletes the statefile. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+my $statefile = "$dpkg_root/var/lib/systemd/deb-systemd-helper-enabled/$random_unit.dsh-also";
+
+ok(-f $statefile, 'state file exists');
+
+$ENV{'_DEB_SYSTEMD_HELPER_PURGE'} = '1';
+$retval = dsh('disable', $random_unit);
+delete $ENV{'_DEB_SYSTEMD_HELPER_PURGE'};
+is($retval, 0, "disable command succeeded");
+ok(! -f $statefile, 'state file does not exist anymore after purging');
+isnt_debian_installed($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “enable” after purging does re-create the symlinks. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+ok(! -l $symlink_path, 'symlink does not exist yet');
+isnt_enabled($random_unit);
+
+$retval = dsh('enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+
+is_enabled($random_unit);
+is_debian_installed($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “disable” removes the symlinks. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+$ENV{'_DEB_SYSTEMD_HELPER_PURGE'} = '1';
+$retval = dsh('disable', $random_unit);
+delete $ENV{'_DEB_SYSTEMD_HELPER_PURGE'};
+is($retval, 0, "disable command succeeded");
+
+isnt_enabled($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “enable” after purging does re-create the symlinks. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+ok(! -l $symlink_path, 'symlink does not exist yet');
+isnt_enabled($random_unit);
+
+$retval = dsh('enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+
+is_enabled($random_unit);
+is_debian_installed($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify the “purge” verb works. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+$retval = dsh('purge', $random_unit);
+is($retval, 0, "purge command succeeded");
+
+isnt_enabled($random_unit);
+isnt_debian_installed($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “enable” after purging does re-create the symlinks. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+ok(! -l $symlink_path, 'symlink does not exist yet');
+isnt_enabled($random_unit);
+
+$retval = dsh('enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+
+is_enabled($random_unit);
+is_debian_installed($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “mask” (when enabled) results in the symlink pointing to /dev/null ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+my $mask_path = "$dpkg_root/etc/systemd/system/$random_unit";
+ok(! -l $mask_path, 'mask link does not exist yet');
+
+$retval = dsh('mask', $random_unit);
+is($retval, 0, "mask command succeeded");
+ok(-l $mask_path, 'mask link exists');
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('unmask', $random_unit);
+is($retval, 0, "unmask command succeeded");
+ok(! -e $mask_path, 'mask link does not exist anymore');
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “mask” (when disabled) works the same way ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+$retval = dsh('disable', $random_unit);
+is($retval, 0, "disable command succeeded");
+ok(! -e $symlink_path, 'symlink no longer exists');
+
+$retval = dsh('mask', $random_unit);
+is($retval, 0, "mask command succeeded");
+ok(-l $mask_path, 'mask link exists');
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('unmask', $random_unit);
+is($retval, 0, "unmask command succeeded");
+ok(! -e $mask_path, 'symlink no longer exists');
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “mask”/unmask don’t do anything when the user already masked. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+ok(! -l $mask_path, 'mask link does not exist yet');
+symlink('/dev/null', $mask_path);
+ok(-l $mask_path, 'mask link exists');
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('mask', $random_unit);
+is($retval, 0, "mask command succeeded");
+ok(-l $mask_path, 'mask link exists');
+is(readlink($mask_path), '/dev/null', 'service still masked');
+
+$retval = dsh('unmask', $random_unit);
+is($retval, 0, "unmask command succeeded");
+ok(-l $mask_path, 'mask link exists');
+is(readlink($mask_path), '/dev/null', 'service still masked');
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “mask”/unmask don’t do anything when the user copied the .service. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+unlink($mask_path);
+
+open($fh, '>', $mask_path);
+print $fh <<'EOT';
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+WantedBy=multi-user.target
+EOT
+close($fh);
+
+ok(-e $mask_path, 'local service file exists');
+ok(! -l $mask_path, 'local service file is not a symlink');
+
+$retval = dsh('mask', $random_unit);
+isnt($retval, -1, 'deb-systemd-helper could be executed');
+ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal');
+is($retval >> 8, 0, 'deb-systemd-helper exited with exit code 0');
+ok(-e $mask_path, 'local service file still exists');
+ok(! -l $mask_path, 'local service file is still not a symlink');
+
+$retval = dsh('unmask', $random_unit);
+isnt($retval, -1, 'deb-systemd-helper could be executed');
+ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal');
+is($retval >> 8, 0, 'deb-systemd-helper exited with exit code 0');
+ok(-e $mask_path, 'local service file still exists');
+ok(! -l $mask_path, 'local service file is still not a symlink');
+
+unlink($mask_path);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify Alias= handling. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+$retval = dsh('purge', $random_unit);
+is($retval, 0, "purge command succeeded");
+
+open($fh, '>', $servicefile_path);
+print $fh <<'EOT';
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+WantedBy=multi-user.target
+Alias=foo\x2dtest.service
+EOT
+close($fh);
+
+isnt_enabled($random_unit);
+isnt_enabled('foo\x2dtest.service');
+my $alias_path = $dpkg_root . '/etc/systemd/system/foo\x2dtest.service';
+ok(! -l $alias_path, 'alias link does not exist yet');
+$retval = dsh('enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link');
+is_enabled($random_unit);
+ok(! -l $mask_path, 'mask link does not exist yet');
+
+$retval = dsh('mask', $random_unit);
+is($retval, 0, "mask command succeeded");
+is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link');
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('unmask', $random_unit);
+is($retval, 0, "unmask command succeeded");
+is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link');
+ok(! -l $mask_path, 'mask link does not exist any more');
+
+$retval = dsh('disable', $random_unit);
+isnt_enabled($random_unit);
+ok(! -l $alias_path, 'alias link does not exist any more');
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify Alias/mask with removed package (as in postrm) ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+$retval = dsh('purge', $random_unit);
+is($retval, 0, "purge command succeeded");
+$retval = dsh('enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link');
+
+unlink($servicefile_path);
+
+$retval = dsh('mask', $random_unit);
+is($retval, 0, "mask command succeeded with uninstalled unit");
+is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link');
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('purge', $random_unit);
+is($retval, 0, "purge command succeeded with uninstalled unit");
+ok(! -l $alias_path, 'alias link does not exist any more');
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('unmask', $random_unit);
+is($retval, 0, "unmask command succeeded with uninstalled unit");
+ok(! -l $mask_path, 'mask link does not exist any more');
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify Alias= to the same unit name ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+open($fh, '>', $servicefile_path);
+print $fh <<"EOT";
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+WantedBy=multi-user.target
+Alias=$random_unit
+EOT
+close($fh);
+
+isnt_enabled($random_unit);
+isnt_enabled('foo\x2dtest.service');
+# note that in this case $alias_path and $mask_path are identical
+$retval = dsh('enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+is_enabled($random_unit);
+# systemctl enable does create the alias link even if it's not needed
+#ok(! -l $mask_path, 'mask link does not exist yet');
+
+unlink($servicefile_path);
+
+$retval = dsh('mask', $random_unit);
+is($retval, 0, "mask command succeeded");
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('unmask', $random_unit);
+is($retval, 0, "unmask command succeeded");
+ok(! -l $mask_path, 'mask link does not exist any more');
+
+$retval = dsh('purge', $random_unit);
+isnt_enabled($random_unit);
+ok(! -l $mask_path, 'mask link does not exist any more');
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify WantedBy and Alias with template unit with DefaultInstance. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+($fh, $servicefile_path) = tempfile('unit\x2dXXXXX',
+ DIR => "$dpkg_root/lib/systemd/system",
+ SUFFIX => '@.service',
+ UNLINK => 1);
+print $fh <<'EOT';
+[Unit]
+Description=template test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+Alias=foo\x2dtest@.service
+Alias=foo\x2dbar@baz.service
+WantedBy=multi-user.target
+DefaultInstance=instance\x2d
+EOT
+close($fh);
+
+$random_unit = basename($servicefile_path);
+my $random_instance = $random_unit;
+$random_instance =~ s/^(.*\@)(\.\w+)$/$1instance\\x2d$2/;
+
+isnt_enabled($random_unit);
+isnt_enabled('foo\x2dtest@.service');
+isnt_enabled('foo\x2dtest@instance\x2d.service');
+isnt_enabled('foo\x2dbar@baz.service');
+isnt_enabled('foo\x2dbar@instance\x2d.service');
+
+my $template_alias_path = $dpkg_root . '/etc/systemd/system/foo\x2dtest@.service';
+my $instance_alias_path = $dpkg_root . '/etc/systemd/system/foo\x2dbar@baz.service';
+my $template_wanted_path = "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit";
+my $instance_wanted_path = "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_instance";
+ok(! -l $template_alias_path, 'template alias link does not exist yet');
+ok(! -l $instance_alias_path, 'instance alias link does not exist yet');
+ok(! -l $template_wanted_path, 'template wanted link does not exist yet');
+ok(! -l $instance_wanted_path, 'instance wanted link does not exist yet');
+$retval = dsh('enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+is($dpkg_root . readlink($template_alias_path), $servicefile_path, 'correct template alias link');
+is($dpkg_root . readlink($instance_alias_path), $servicefile_path, 'correct instance alias link');
+ok(! -l $template_wanted_path, 'template wanted link does still not exist');
+is($dpkg_root . readlink($instance_wanted_path), $servicefile_path, 'correct instance wanted link');
+is_enabled($random_unit);
+
+$retval = dsh('disable', $random_unit);
+isnt_enabled($random_unit);
+ok(! -l $template_alias_path, 'template alias link does not exist anymore');
+ok(! -l $instance_alias_path, 'instance alias link does not exist anymore');
+ok(! -l $template_wanted_path, 'template wanted link does still not exist');
+ok(! -l $instance_wanted_path, 'instance wanted link does not exist anymore');
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify WantedBy and Alias with template unit without DefaultInstance. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+open($fh, '>', $servicefile_path);
+print $fh <<'EOT';
+[Unit]
+Description=template test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+Alias=foo\x2dtest@.service
+Alias=foo\x2dbar@baz.service
+RequiredBy=foo\x2ddepender@.service
+EOT
+close($fh);
+
+isnt_enabled($random_unit);
+isnt_enabled('foo\x2dtest@.service');
+isnt_enabled('foo\x2dbar@baz.service');
+
+$template_alias_path = $dpkg_root . '/etc/systemd/system/foo\x2dtest@.service';
+$instance_alias_path = $dpkg_root . '/etc/systemd/system/foo\x2dbar@baz.service';
+$template_wanted_path = $dpkg_root . '/etc/systemd/system/foo\x2ddepender@.service.requires/' . $random_unit;
+$instance_wanted_path = $dpkg_root . '/etc/systemd/system/foo\x2ddepender@.service.requires/' . $random_instance;
+ok(! -l $template_alias_path, 'template alias link does not exist yet');
+ok(! -l $instance_alias_path, 'instance alias link does not exist yet');
+ok(! -l $template_wanted_path, 'template wanted link does not exist yet');
+ok(! -l $instance_wanted_path, 'instance wanted link does not exist yet');
+$retval = dsh('enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+is($dpkg_root . readlink($template_alias_path), $servicefile_path, 'correct template alias link');
+is($dpkg_root . readlink($instance_alias_path), $servicefile_path, 'correct instance alias link');
+is($dpkg_root . readlink($template_wanted_path), $servicefile_path, 'correct template wanted link');
+ok(! -l $instance_wanted_path, 'instance wanted link does still not exist');
+is_enabled($random_unit);
+
+$retval = dsh('disable', $random_unit);
+isnt_enabled($random_unit);
+ok(! -l $template_alias_path, 'template alias link does not exist anymore');
+ok(! -l $instance_alias_path, 'instance alias link does not exist anymore');
+ok(! -l $template_wanted_path, 'template wanted link does still not exist');
+ok(! -l $instance_wanted_path, 'instance wanted link does not exist anymore');
+
+done_testing;
diff --git a/t/002-deb-systemd-helper-update.t b/t/002-deb-systemd-helper-update.t
new file mode 100644
index 0000000..7f7d826
--- /dev/null
+++ b/t/002-deb-systemd-helper-update.t
@@ -0,0 +1,196 @@
+#!perl
+# vim:ts=4:sw=4:et
+
+use strict;
+use warnings;
+use Test::More;
+use Test::Deep qw(:preload cmp_bag);
+use File::Temp qw(tempfile tempdir); # in core since perl 5.6.1
+use File::Path qw(make_path); # in core since Perl 5.001
+use File::Basename; # in core since Perl 5
+use FindBin; # in core since Perl 5.00307
+
+use lib "$FindBin::Bin/.";
+use helpers;
+
+test_setup();
+
+my $dpkg_root = $ENV{DPKG_ROOT} // '';
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “is-enabled” is not true for a random, non-existing unit file. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+my ($fh, $random_unit) = tempfile('unitXXXXX',
+ SUFFIX => '.service',
+ TMPDIR => 1,
+ UNLINK => 1);
+close($fh);
+$random_unit = basename($random_unit);
+
+my $statefile = "$dpkg_root/var/lib/systemd/deb-systemd-helper-enabled/$random_unit.dsh-also";
+my $servicefile_path = "$dpkg_root/lib/systemd/system/$random_unit";
+make_path("$dpkg_root/lib/systemd/system");
+open($fh, '>', $servicefile_path);
+print $fh <<'EOT';
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+WantedBy=multi-user.target
+EOT
+close($fh);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “enable” creates the requested symlinks. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+my $retval = dsh('enable', $random_unit);
+my $symlink_path = "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit";
+ok(-l $symlink_path, "$random_unit was enabled");
+is($dpkg_root . readlink($symlink_path), $servicefile_path,
+ "symlink points to $servicefile_path");
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “is-enabled” now returns true. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+is_enabled($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Modify the unit file and verify that “is-enabled” is no longer true. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+open($fh, '>>', $servicefile_path);
+print $fh "Alias=newalias.service\n";
+close($fh);
+
+isnt_enabled($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “was-enabled” is still true (operates on the state file). ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+$retval = dsh('was-enabled', $random_unit);
+isnt($retval, -1, 'deb-systemd-helper could be executed');
+ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal');
+is($retval >> 8, 0, "random unit file was-enabled");
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify the new symlink is not yet in the state file. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+is_deeply(
+ [ state_file_entries($statefile) ],
+ [ $symlink_path ],
+ 'state file does not contain the new link yet');
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “enable” creates the new symlinks. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+my $new_symlink_path = "$dpkg_root/etc/systemd/system/newalias.service";
+ok(! -l $new_symlink_path, 'new symlink does not exist yet');
+
+$retval = dsh('enable', $random_unit);
+ok(-l $new_symlink_path, 'new symlink was created');
+is($dpkg_root . readlink($new_symlink_path), $servicefile_path,
+ "symlink points to $servicefile_path");
+
+is_enabled($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify the new symlink was recorded in the state file. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+cmp_bag(
+ [ state_file_entries($statefile) ],
+ [ $symlink_path, $new_symlink_path ],
+ 'state file updated');
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Modify the unit file and verify that “is-enabled” is no longer true. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+open($fh, '>>', $servicefile_path);
+print $fh "Alias=another.service\n";
+close($fh);
+
+isnt_enabled($random_unit);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “was-enabled” is still true (operates on the state file). ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+$retval = dsh('was-enabled', $random_unit);
+isnt($retval, -1, 'deb-systemd-helper could be executed');
+ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal');
+is($retval >> 8, 0, "random unit file was-enabled");
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify the new symlink is not yet in the state file. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+cmp_bag(
+ [ state_file_entries($statefile) ],
+ [ $symlink_path, $new_symlink_path ],
+ 'state file does not contain the new link yet');
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “update-state” does not create the symlink, but records it in the ┃
+# ┃ state file. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+my $new_symlink_path2 = "$dpkg_root/etc/systemd/system/another.service";
+ok(! -l $new_symlink_path2, 'new symlink does not exist yet');
+
+$retval = dsh('update-state', $random_unit);
+ok(! -l $new_symlink_path2, 'new symlink still does not exist');
+
+isnt_enabled($random_unit);
+
+cmp_bag(
+ [ state_file_entries($statefile) ],
+ [ $symlink_path, $new_symlink_path, $new_symlink_path2 ],
+ 'state file updated');
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Rewrite the original contents and verify “update-state” removes the old ┃
+# ┃ links that are no longer present. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+open($fh, '>', $servicefile_path);
+print $fh <<'EOT';
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+WantedBy=multi-user.target
+EOT
+close($fh);
+
+unlink($new_symlink_path);
+
+ok(! -l $new_symlink_path, 'new symlink still does not exist');
+ok(! -l $new_symlink_path2, 'new symlink 2 still does not exist');
+
+$retval = dsh('update-state', $random_unit);
+
+ok(! -l $new_symlink_path, 'new symlink still does not exist');
+ok(! -l $new_symlink_path2, 'new symlink 2 still does not exist');
+
+is_enabled($random_unit);
+
+is_deeply(
+ [ state_file_entries($statefile) ],
+ [ $symlink_path ],
+ 'state file updated');
+
+
+done_testing;
diff --git a/t/003-deb-systemd-helper-complex.t b/t/003-deb-systemd-helper-complex.t
new file mode 100644
index 0000000..47d8fb4
--- /dev/null
+++ b/t/003-deb-systemd-helper-complex.t
@@ -0,0 +1,123 @@
+#!perl
+# vim:ts=4:sw=4:et
+
+use strict;
+use warnings;
+use Test::More;
+use Test::Deep qw(:preload cmp_bag);
+use File::Temp qw(tempfile tempdir); # in core since perl 5.6.1
+use File::Path qw(make_path); # in core since Perl 5.001
+use File::Basename; # in core since Perl 5
+use FindBin; # in core since Perl 5.00307
+
+use lib "$FindBin::Bin/.";
+use helpers;
+
+test_setup();
+
+my $dpkg_root = $ENV{DPKG_ROOT} // '';
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Create two unit files with random names; they refer to each other (Also=).┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+my ($fh1, $random_unit1) = tempfile('unitXXXXX',
+ SUFFIX => '.service',
+ TMPDIR => 1,
+ UNLINK => 1);
+close($fh1);
+$random_unit1 = basename($random_unit1);
+
+my ($fh2, $random_unit2) = tempfile('unitXXXXX',
+ SUFFIX => '.service',
+ TMPDIR => 1,
+ UNLINK => 1);
+close($fh2);
+$random_unit2 = basename($random_unit2);
+
+my $servicefile_path1 = "$dpkg_root/lib/systemd/system/$random_unit1";
+my $servicefile_path2 = "$dpkg_root/lib/systemd/system/$random_unit2";
+make_path("$dpkg_root/lib/systemd/system");
+open($fh1, '>', $servicefile_path1);
+print $fh1 <<EOT;
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+WantedBy=multi-user.target
+Also=$random_unit2
+EOT
+close($fh1);
+
+open($fh2, '>', $servicefile_path2);
+print $fh2 <<EOT;
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+WantedBy=multi-user.target
+Alias=alias2.service
+Also=$random_unit1
+EOT
+close($fh2);
+
+isnt_enabled($random_unit1);
+isnt_enabled($random_unit2);
+isnt_debian_installed($random_unit1);
+isnt_debian_installed($random_unit2);
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “enable” creates all symlinks. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+unless ($ENV{'TEST_ON_REAL_SYSTEM'}) {
+ # This might already exist if we don't start from a fresh directory
+ ok(! -d "$dpkg_root/etc/systemd/system/multi-user.target.wants",
+ 'multi-user.target.wants does not exist yet');
+}
+
+my $retval = dsh('enable', $random_unit1);
+my %links = map { (basename($_), $dpkg_root . readlink($_)) }
+ ("$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit1",
+ "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit2");
+is_deeply(
+ \%links,
+ {
+ $random_unit1 => $servicefile_path1,
+ $random_unit2 => $servicefile_path2,
+ },
+ 'All expected links present');
+
+my $alias_path = "$dpkg_root/etc/systemd/system/alias2.service";
+ok(-l $alias_path, 'alias created');
+is($dpkg_root . readlink($alias_path), $servicefile_path2,
+ 'alias points to the correct service file');
+
+cmp_bag(
+ [ state_file_entries("$dpkg_root/var/lib/systemd/deb-systemd-helper-enabled/$random_unit1.dsh-also") ],
+ [ "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit1",
+ "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit2",
+ "$dpkg_root/etc/systemd/system/alias2.service" ],
+ 'state file updated');
+
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ Verify “is-enabled” now returns true. ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+is_enabled($random_unit1);
+is_enabled($random_unit2);
+is_debian_installed($random_unit1);
+
+# $random_unit2 was only enabled _because of_ $random_unit1’s Also= statement
+# and thus does not have its own state file.
+isnt_debian_installed($random_unit2);
+
+# TODO: cleanup tests?
+
+done_testing;
diff --git a/t/004-deb-systemd-helper-user.t b/t/004-deb-systemd-helper-user.t
new file mode 100644
index 0000000..bd914cb
--- /dev/null
+++ b/t/004-deb-systemd-helper-user.t
@@ -0,0 +1,413 @@
+#!perl
+# vim:ts=4:sw=4:et
+
+use strict;
+use warnings;
+use Test::More;
+use File::Temp qw(tempfile tempdir); # in core since perl 5.6.1
+use File::Path qw(make_path); # in core since Perl 5.001
+use File::Basename; # in core since Perl 5
+use FindBin; # in core since Perl 5.00307
+
+use lib "$FindBin::Bin/.";
+use helpers;
+
+test_setup();
+
+my $dpkg_root = $ENV{DPKG_ROOT} // '';
+
+#
+# "is-enabled" is not true for a random, non-existing unit file
+#
+
+my ($fh, $random_unit) = tempfile('unit\x2dXXXXX',
+ SUFFIX => '.service',
+ TMPDIR => 1,
+ UNLINK => 1);
+close($fh);
+$random_unit = basename($random_unit);
+
+isnt_enabled($random_unit);
+isnt_enabled($random_unit, user => 1);
+isnt_debian_installed($random_unit);
+isnt_debian_installed($random_unit, user => 1);
+
+#
+# "is-enabled" is not true for a random, existing user unit file
+#
+
+my $servicefile_path = "$dpkg_root/usr/lib/systemd/user/$random_unit";
+make_path("$dpkg_root/usr/lib/systemd/user");
+open($fh, '>', $servicefile_path);
+print $fh <<'EOT';
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+WantedBy=default.target
+EOT
+close($fh);
+
+isnt_enabled($random_unit);
+isnt_enabled($random_unit, user => 1);
+isnt_debian_installed($random_unit);
+isnt_debian_installed($random_unit, user => 1);
+
+#
+# "enable" creates the requested symlinks
+#
+
+unless ($ENV{'TEST_ON_REAL_SYSTEM'}) {
+ # This might exist if we don't start from a fresh directory
+ ok(! -d "$dpkg_root/etc/systemd/user/default.target.wants",
+ 'default.target.wants does not exist yet');
+}
+
+my $retval = dsh('--user', 'enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+my $symlink_path = "$dpkg_root/etc/systemd/user/default.target.wants/$random_unit";
+ok(-l $symlink_path, "$random_unit was enabled");
+is($dpkg_root . readlink($symlink_path), $servicefile_path,
+ "symlink points to $servicefile_path");
+
+#
+# "is-enabled" now returns true for the user instance
+#
+
+isnt_enabled($random_unit);
+isnt_debian_installed($random_unit);
+is_enabled($random_unit, user => 1);
+is_debian_installed($random_unit, user => 1);
+
+#
+# deleting the symlinks and running "enable" again does not re-create them
+#
+
+unlink($symlink_path);
+ok(! -l $symlink_path, 'symlink deleted');
+isnt_enabled($random_unit);
+isnt_enabled($random_unit, user => 1);
+isnt_debian_installed($random_unit);
+is_debian_installed($random_unit, user => 1);
+
+$retval = dsh('--user', 'enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+
+isnt_enabled($random_unit, user => 1);
+
+#
+# "disable" deletes the statefile when purging
+#
+
+my $statefile = "$dpkg_root/var/lib/systemd/deb-systemd-user-helper-enabled/$random_unit.dsh-also";
+
+ok(-f $statefile, 'state file exists');
+
+$ENV{'_DEB_SYSTEMD_HELPER_PURGE'} = '1';
+$retval = dsh('--user', 'disable', $random_unit);
+delete $ENV{'_DEB_SYSTEMD_HELPER_PURGE'};
+is($retval, 0, "disable command succeeded");
+ok(! -f $statefile, 'state file does not exist anymore after purging');
+isnt_debian_installed($random_unit);
+isnt_debian_installed($random_unit, user => 1);
+
+#
+# "enable" re-creates the symlinks after purging
+#
+
+ok(! -l $symlink_path, 'symlink does not exist yet');
+isnt_enabled($random_unit);
+isnt_enabled($random_unit, user => 1);
+
+$retval = dsh('--user', 'enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+
+isnt_enabled($random_unit);
+isnt_debian_installed($random_unit);
+is_enabled($random_unit, user => 1);
+is_debian_installed($random_unit, user => 1);
+
+#
+# "disable" removes the symlinks
+#
+
+$ENV{'_DEB_SYSTEMD_HELPER_PURGE'} = '1';
+$retval = dsh('--user', 'disable', $random_unit);
+delete $ENV{'_DEB_SYSTEMD_HELPER_PURGE'};
+is($retval, 0, "disable command succeeded");
+
+isnt_enabled($random_unit);
+isnt_enabled($random_unit, user => 1);
+
+#
+# "enable" re-creates the symlinks
+#
+
+ok(! -l $symlink_path, 'symlink does not exist yet');
+isnt_enabled($random_unit);
+isnt_enabled($random_unit, user => 1);
+
+$retval = dsh('--user', 'enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+
+isnt_enabled($random_unit);
+isnt_debian_installed($random_unit);
+is_enabled($random_unit, user => 1);
+is_debian_installed($random_unit, user => 1);
+
+#
+# "purge" works
+#
+
+$retval = dsh('--user', 'purge', $random_unit);
+is($retval, 0, "purge command succeeded");
+
+isnt_enabled($random_unit, user => 1);
+isnt_debian_installed($random_unit, user => 1);
+
+#
+# "enable" re-creates the symlinks after purging
+#
+
+ok(! -l $symlink_path, 'symlink does not exist yet');
+isnt_enabled($random_unit, user => 1);
+
+$retval = dsh('--user', 'enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+
+is_enabled($random_unit, user => 1);
+is_debian_installed($random_unit, user => 1);
+
+#
+# "mask" (when enabled) results in the symlink pointing to /dev/null
+#
+
+my $mask_path = "$dpkg_root/etc/systemd/user/$random_unit";
+ok(! -l $mask_path, 'mask link does not exist yet');
+
+$retval = dsh('--user', 'mask', $random_unit);
+is($retval, 0, "mask command succeeded");
+ok(-l $mask_path, 'mask link exists');
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('--user', 'unmask', $random_unit);
+is($retval, 0, "unmask command succeeded");
+ok(! -e $mask_path, 'mask link does not exist anymore');
+
+#
+# "mask" (when disabled) works the same way
+#
+
+$retval = dsh('--user', 'disable', $random_unit);
+is($retval, 0, "disable command succeeded");
+ok(! -e $symlink_path, 'symlink no longer exists');
+
+$retval = dsh('--user', 'mask', $random_unit);
+is($retval, 0, "mask command succeeded");
+ok(-l $mask_path, 'mask link exists');
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('--user', 'unmask', $random_unit);
+is($retval, 0, "unmask command succeeded");
+ok(! -e $mask_path, 'symlink no longer exists');
+
+#
+# "mask" / "unmask" don't do anything when the unit is already masked
+#
+
+ok(! -l $mask_path, 'mask link does not exist yet');
+symlink('/dev/null', $mask_path);
+ok(-l $mask_path, 'mask link exists');
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('--user', 'mask', $random_unit);
+is($retval, 0, "mask command succeeded");
+ok(-l $mask_path, 'mask link exists');
+is(readlink($mask_path), '/dev/null', 'service still masked');
+
+$retval = dsh('--user', 'unmask', $random_unit);
+is($retval, 0, "unmask command succeeded");
+ok(-l $mask_path, 'mask link exists');
+is(readlink($mask_path), '/dev/null', 'service still masked');
+
+#
+# "mask" / "unmask" don't do anything when the user copied the .service.
+#
+
+unlink($mask_path);
+
+open($fh, '>', $mask_path);
+print $fh <<'EOT';
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+WantedBy=default.target
+EOT
+close($fh);
+
+ok(-e $mask_path, 'local service file exists');
+ok(! -l $mask_path, 'local service file is not a symlink');
+
+$retval = dsh('--user', 'mask', $random_unit);
+isnt($retval, -1, 'deb-systemd-helper could be executed');
+ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal');
+is($retval >> 8, 0, 'deb-systemd-helper exited with exit code 0');
+ok(-e $mask_path, 'local service file still exists');
+ok(! -l $mask_path, 'local service file is still not a symlink');
+
+$retval = dsh('--user', 'unmask', $random_unit);
+isnt($retval, -1, 'deb-systemd-helper could be executed');
+ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal');
+is($retval >> 8, 0, 'deb-systemd-helper exited with exit code 0');
+ok(-e $mask_path, 'local service file still exists');
+ok(! -l $mask_path, 'local service file is still not a symlink');
+
+unlink($mask_path);
+
+#
+# "Alias=" handling
+#
+
+$retval = dsh('--user', 'purge', $random_unit);
+is($retval, 0, "purge command succeeded");
+
+open($fh, '>', $servicefile_path);
+print $fh <<'EOT';
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+WantedBy=default.target
+Alias=foo\x2dtest.service
+EOT
+close($fh);
+
+isnt_enabled($random_unit, user => 1);
+isnt_enabled('foo\x2dtest.service', user => 1);
+my $alias_path = $dpkg_root . '/etc/systemd/user/foo\x2dtest.service';
+ok(! -l $alias_path, 'alias link does not exist yet');
+$retval = dsh('--user', 'enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link');
+is_enabled($random_unit, user => 1);
+ok(! -l $mask_path, 'mask link does not exist yet');
+
+$retval = dsh('--user', 'mask', $random_unit);
+is($retval, 0, "mask command succeeded");
+is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link');
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('--user', 'unmask', $random_unit);
+is($retval, 0, "unmask command succeeded");
+is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link');
+ok(! -l $mask_path, 'mask link does not exist any more');
+
+$retval = dsh('--user', 'disable', $random_unit);
+isnt_enabled($random_unit, user => 1);
+ok(! -l $alias_path, 'alias link does not exist any more');
+
+#
+# "Alias=" / "mask" with removed package (as in postrm)
+#
+
+$retval = dsh('--user', 'purge', $random_unit);
+is($retval, 0, "purge command succeeded");
+$retval = dsh('--user', 'enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link');
+
+unlink($servicefile_path);
+
+$retval = dsh('--user', 'mask', $random_unit);
+is($retval, 0, "mask command succeeded with uninstalled unit");
+is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link');
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('--user', 'purge', $random_unit);
+is($retval, 0, "purge command succeeded with uninstalled unit");
+ok(! -l $alias_path, 'alias link does not exist any more');
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('--user', 'unmask', $random_unit);
+is($retval, 0, "unmask command succeeded with uninstalled unit");
+ok(! -l $mask_path, 'mask link does not exist any more');
+
+#
+# "Alias=" to the same unit name
+#
+
+open($fh, '>', $servicefile_path);
+print $fh <<"EOT";
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+WantedBy=default.target
+Alias=$random_unit
+EOT
+close($fh);
+
+isnt_enabled($random_unit, user => 1);
+isnt_enabled('foo\x2dtest.service', user => 1);
+# note that in this case $alias_path and $mask_path are identical
+$retval = dsh('--user', 'enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+is_enabled($random_unit, user => 1);
+# systemctl enable does create the alias link even if it's not needed
+#ok(! -l $mask_path, 'mask link does not exist yet');
+
+unlink($servicefile_path);
+
+$retval = dsh('--user', 'mask', $random_unit);
+is($retval, 0, "mask command succeeded");
+is(readlink($mask_path), '/dev/null', 'service masked');
+
+$retval = dsh('--user', 'unmask', $random_unit);
+is($retval, 0, "unmask command succeeded");
+ok(! -l $mask_path, 'mask link does not exist any more');
+
+$retval = dsh('--user', 'purge', $random_unit);
+isnt_enabled($random_unit, user => 1);
+ok(! -l $mask_path, 'mask link does not exist any more');
+
+#
+# "Alias=" without "WantedBy="
+#
+
+open($fh, '>', $servicefile_path);
+print $fh <<'EOT';
+[Unit]
+Description=test unit
+
+[Service]
+ExecStart=/bin/sleep 1
+
+[Install]
+Alias=baz\x2dtest.service
+EOT
+close($fh);
+
+isnt_enabled($random_unit, user => 1);
+isnt_enabled('baz\x2dtest.service', user => 1);
+$alias_path = $dpkg_root . '/etc/systemd/user/baz\x2dtest.service';
+ok(! -l $alias_path, 'alias link does not exist yet');
+$retval = dsh('--user', 'enable', $random_unit);
+is($retval, 0, "enable command succeeded");
+is_enabled($random_unit, user => 1);
+ok(-l $alias_path, 'alias link does exist');
+is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link');
+
+done_testing;
diff --git a/t/README b/t/README
new file mode 100644
index 0000000..eda327d
--- /dev/null
+++ b/t/README
@@ -0,0 +1,15 @@
+These test cases need the Linux::Clone module, which is not yet in Debian.
+See http://michael.stapelberg.de/cpan/#Linux::Clone on how to install it.
+
+Note that you need to run the test cases as root because they use Linux mount
+namespaces and bind mounts (requires Linux ≥ 2.4.19).
+
+Note that you should mark / as a private subtree before running these tests, or
+they will fail. Use mount --make-rprivate /. Unfortunately, the version of
+util-linux in Debian at the time of writing (2.20.1) is broken and its
+--make-rprivate does not actually work. See #731574.
+
+The intention is that the testcases are _not_ run automatically during package
+building because they might be too fragile and additional dependencies make it
+harder to port this package to Ubuntu or Debian backports. It is enough if the
+test cases are run on every code change.
diff --git a/t/helpers.pm b/t/helpers.pm
new file mode 100644
index 0000000..72d4f68
--- /dev/null
+++ b/t/helpers.pm
@@ -0,0 +1,160 @@
+use strict;
+use warnings;
+use English;
+use File::Temp qw(tempdir); # in core since perl 5.6.1
+use File::Copy qw(cp);
+use File::Path qw(make_path);
+
+sub check_fakechroot_running() {
+ my $content = `FAKECHROOT_DETECT=1 sh -c "echo This should not be printed"`;
+ my $result = 0;
+ if ($content =~ /^fakechroot [0-9.]+\n$/) {
+ $result = 1;
+ }
+ return $result;
+}
+
+sub test_setup() {
+ if (length $ENV{TEST_DPKG_ROOT}) {
+ print STDERR "test_setup() with DPKG_ROOT\n";
+ $ENV{DPKG_ROOT} = tempdir( CLEANUP => 1 );
+ return;
+ }
+
+ if ( !check_fakechroot_running ) {
+ print STDERR "you have to run this script inside fakechroot and fakeroot:\n";
+ print STDERR (" fakechroot fakeroot perl $PROGRAM_NAME" . (join " ", @ARGV) . "\n");
+ exit 1;
+ }
+
+ # Set up a chroot that contains everything necessary to run
+ # deb-systemd-helper under fakechroot.
+ print STDERR "test_setup() with fakechroot\n";
+
+ my $tmpdir = tempdir( CLEANUP => 1 );
+ mkdir "$tmpdir/dev";
+ 0 == system 'mknod', "$tmpdir/dev/null", 'c', '1', '3' or die "cannot mknod: $?";
+ mkdir "$tmpdir/tmp";
+ make_path("$tmpdir/usr/bin");
+ make_path("$tmpdir/usr/lib/systemd/user");
+ make_path("$tmpdir/lib/systemd/system/");
+ make_path("$tmpdir/var/lib/systemd");
+ make_path("$tmpdir/etc/systemd");
+ if ( length $ENV{TEST_INSTALLED} ) {
+ # if we test the installed deb-systemd-helper we copy it from the
+ # system's installation
+ cp "/usr/bin/deb-systemd-helper", "$tmpdir/usr/bin/deb-systemd-helper"
+ or die "cannot copy: $!";
+ }
+ else {
+ cp "$FindBin::Bin/../script/deb-systemd-helper",
+ "$tmpdir/usr/bin/deb-systemd-helper"
+ or die "cannot copy: $!";
+ }
+
+ # make sure that dpkg diversion messages are not translated
+ local $ENV{LC_ALL} = 'C.UTF-8';
+ # the chroot only needs to contain a working perl-base
+ open my $fh, '-|', 'dpkg-query', '--listfiles', 'perl-base';
+
+ while ( my $path = <$fh> ) {
+ chomp $path;
+ # filter out diversion messages in the same way that dpkg-repack does
+ # https://git.dpkg.org/cgit/dpkg/dpkg-repack.git/tree/dpkg-repack#n238
+ if ($path =~ /^package diverts others to: /) {
+ next;
+ }
+ if ($path =~ /^diverted by [^ ]+ to: /) {
+ next;
+ }
+ if ($path =~ /^locally diverted to: /) {
+ next;
+ }
+ if ($path !~ /^\//) {
+ die "path must start with a slash";
+ }
+ if ( -e "$tmpdir$path" ) {
+ # ignore paths that were already created
+ next;
+ } elsif ( !-r $path ) {
+ # if the host's path is not readable, assume it's a directory
+ mkdir "$tmpdir$path" or die "cannot mkdir $path: $!";
+ } elsif ( -l $path ) {
+ symlink readlink($path), "$tmpdir$path";
+ } elsif ( -d $path ) {
+ mkdir "$tmpdir$path" or die "cannot mkdir $path: $!";
+ } elsif ( -f $path ) {
+ cp $path, "$tmpdir$path" or die "cannot cp $path: $!";
+ } else {
+ die "cannot handle $path";
+ }
+ }
+ close $fh;
+
+ $ENV{'SYSTEMCTL_INSTALL_CLIENT_SIDE'} = '1';
+
+ # we run the chroot call in a child process because we need the parent
+ # process remaining un-chrooted or otherwise it cannot clean-up the
+ # temporary directory on exit
+ my $pid = fork() // die "cannot fork: $!";
+ if ( $pid == 0 ) {
+ chroot $tmpdir or die "cannot chroot: $!";
+ chdir "/" or die "cannot chdir to /: $!";
+ return;
+ }
+ waitpid($pid, 0);
+
+ exit $?;
+}
+
+# reads in a whole file
+sub slurp {
+ open my $fh, '<', shift;
+ local $/;
+ <$fh>;
+}
+
+sub state_file_entries {
+ my ($path) = @_;
+ my $bytes = slurp($path);
+ my $dpkg_root = $ENV{DPKG_ROOT} // '';
+ return map { "$dpkg_root$_" } split("\n", $bytes);
+}
+
+my $dsh = '';
+if ( length $ENV{TEST_INSTALLED} ) {
+ # if we are to test the installed version of deb-systemd-helper then even
+ # in DPKG_ROOT mode, we want to run /usr/bin/deb-systemd-helper
+ $dsh = "/usr/bin/deb-systemd-helper";
+} else {
+ if ( length $ENV{TEST_DPKG_ROOT} ) {
+ # when testing deb-systemd-helper from source, then in DPKG_ROOT mode,
+ # we take the script from the source directory
+ $dsh = "$FindBin::Bin/../script/deb-systemd-helper";
+ } else {
+ $dsh = "/usr/bin/deb-systemd-helper";
+ }
+}
+$ENV{'DPKG_MAINTSCRIPT_PACKAGE'} = 'deb-systemd-helper-test';
+
+sub dsh {
+ return system($dsh, @_);
+}
+
+sub _unit_check {
+ my ($cmd, $cb, $verb, $unit, %opts) = @_;
+
+ my $retval = dsh($opts{'user'} ? '--user' : '--system', $cmd, $unit);
+
+ isnt($retval, -1, 'deb-systemd-helper could be executed');
+ ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal');
+ $cb->($retval >> 8, 0, "random unit file '$unit' $verb $cmd");
+}
+
+sub is_enabled { _unit_check('is-enabled', \&is, 'is', @_) }
+sub isnt_enabled { _unit_check('is-enabled', \&isnt, 'isnt', @_) }
+
+sub is_debian_installed { _unit_check('debian-installed', \&is, 'is', @_) }
+sub isnt_debian_installed { _unit_check('debian-installed', \&isnt, 'isnt', @_) }
+
+1;