aboutsummaryrefslogtreecommitdiff
path: root/tests/installer-steps/test_hyprlock_pam.py
blob: 8d7295fa914323111fc2d8a580e831a23a658d93 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
"""Tests for configure_hyprlock_pam in the archsetup installer.

hyprlock's packaged /etc/pam.d/hyprlock ships only `auth include login`, with no
account or session sections. pam_end() then crashes cleaning up handles that
were never initialised -- one of the documented causes of the hyprlock lockout
where the session wedges with no prompt (hyprlock#953). configure_hyprlock_pam
writes a complete stack so the account and session phases run.

These tests exercise the REAL function body, extracted from the `archsetup`
script at run time, against a fixture PAM path.

Run from repo root:
    python3 -m unittest tests.installer-steps.test_hyprlock_pam
"""

import os
import subprocess
import tempfile
import unittest

REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
ARCHSETUP = os.path.join(REPO_ROOT, "archsetup")


def run(path):
    script = (
        'logfile=/dev/null\n'
        'display() { :; }\n'
        'backup_system_file() { :; }\n'
        'error_warn() { echo "WARN:$1"; return 1; }\n'
        "source <(sed -n '/^configure_hyprlock_pam() {/,/^}/p' \"%s\")\n"
        'configure_hyprlock_pam "%s"\n'
    ) % (ARCHSETUP, path)
    return subprocess.run(["bash", "-c", script], capture_output=True,
                          text=True, timeout=10)


class ConfigureHyprlockPam(unittest.TestCase):
    def _write(self, initial="auth include login\n"):
        fd, path = tempfile.mkstemp(prefix="pam-hyprlock-")
        os.close(fd)
        with open(path, "w") as f:
            f.write(initial)
        self.addCleanup(os.remove, path)
        return path

    def _stack(self, path):
        with open(path) as f:
            return [l.split()[0] for l in f if l.strip()
                    and not l.lstrip().startswith("#")]

    # ---------------------------------------------------------- normal ----
    def test_writes_all_three_phases(self):
        path = self._write()
        run(path)
        phases = set(self._stack(path))
        self.assertEqual(phases, {"auth", "account", "session"},
                         "the complete stack must carry all three PAM phases")

    def test_account_and_session_are_the_added_ones(self):
        path = self._write()
        run(path)
        stack = self._stack(path)
        self.assertIn("account", stack)
        self.assertIn("session", stack)

    # -------------------------------------------------------- boundary ----
    def test_idempotent_on_rerun(self):
        path = self._write()
        run(path)
        first = open(path).read()
        run(path)
        self.assertEqual(open(path).read(), first,
                         "a second run must not stack duplicate lines")

    def test_overwrites_the_incomplete_package_default(self):
        # The package default is exactly `auth include login`; a re-run replaces
        # it wholesale rather than appending to it.
        path = self._write("auth include login\n")
        run(path)
        self.assertEqual(self._stack(path).count("auth"), 1)


if __name__ == "__main__":
    unittest.main()