test_dbus.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. # systemctl-mqtt - MQTT client triggering & reporting shutdown on systemd-based systems
  2. #
  3. # Copyright (C) 2020 Fabian Peter Hammerle <fabian@hammerle.me>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. import asyncio
  18. import datetime
  19. import logging
  20. import typing
  21. import unittest.mock
  22. import jeepney
  23. import jeepney.low_level
  24. import jeepney.wrappers
  25. import pytest
  26. import systemctl_mqtt._dbus.login_manager
  27. # pylint: disable=protected-access
  28. def test_get_login_manager_proxy():
  29. login_manager = systemctl_mqtt._dbus.login_manager.get_login_manager_proxy()
  30. assert isinstance(login_manager, jeepney.io.blocking.Proxy)
  31. assert login_manager._msggen.interface == "org.freedesktop.login1.Manager"
  32. # https://freedesktop.org/wiki/Software/systemd/logind/
  33. assert login_manager.CanPowerOff() in {("yes",), ("challenge",)}
  34. def test__log_shutdown_inhibitors_some(caplog):
  35. login_manager = unittest.mock.MagicMock()
  36. login_manager.ListInhibitors.return_value = (
  37. [
  38. (
  39. "shutdown:sleep",
  40. "Developer",
  41. "Haven't pushed my commits yet",
  42. "delay",
  43. 1000,
  44. 1234,
  45. ),
  46. ("shutdown", "Editor", "", "Unsafed files open", 0, 42),
  47. ],
  48. )
  49. with caplog.at_level(logging.DEBUG):
  50. systemctl_mqtt._dbus.login_manager._log_shutdown_inhibitors(login_manager)
  51. assert len(caplog.records) == 2
  52. assert caplog.records[0].levelno == logging.DEBUG
  53. assert (
  54. caplog.records[0].message
  55. == "detected shutdown inhibitor Developer (pid=1234, uid=1000, mode=delay): "
  56. + "Haven't pushed my commits yet"
  57. )
  58. def test__log_shutdown_inhibitors_none(caplog):
  59. login_manager = unittest.mock.MagicMock()
  60. login_manager.ListInhibitors.return_value = ([],)
  61. with caplog.at_level(logging.DEBUG):
  62. systemctl_mqtt._dbus.login_manager._log_shutdown_inhibitors(login_manager)
  63. assert len(caplog.records) == 1
  64. assert caplog.records[0].levelno == logging.DEBUG
  65. assert caplog.records[0].message == "no shutdown inhibitor locks found"
  66. def test__log_shutdown_inhibitors_fail(caplog):
  67. login_manager = unittest.mock.MagicMock()
  68. login_manager.ListInhibitors.side_effect = DBusErrorResponseMock("error", "mocked")
  69. with caplog.at_level(logging.DEBUG):
  70. systemctl_mqtt._dbus.login_manager._log_shutdown_inhibitors(login_manager)
  71. assert len(caplog.records) == 1
  72. assert caplog.records[0].levelno == logging.WARNING
  73. assert (
  74. caplog.records[0].message
  75. == "failed to fetch shutdown inhibitors: [error] mocked"
  76. )
  77. @pytest.mark.parametrize("action", ["poweroff", "reboot"])
  78. @pytest.mark.parametrize("delay", [datetime.timedelta(0), datetime.timedelta(hours=1)])
  79. def test__schedule_shutdown(action, delay):
  80. login_manager_mock = unittest.mock.MagicMock()
  81. with unittest.mock.patch(
  82. "systemctl_mqtt._dbus.login_manager.get_login_manager_proxy",
  83. return_value=login_manager_mock,
  84. ):
  85. login_manager_mock.ListInhibitors.return_value = ([],)
  86. systemctl_mqtt._dbus.login_manager.schedule_shutdown(action=action, delay=delay)
  87. login_manager_mock.ScheduleShutdown.assert_called_once()
  88. schedule_args, schedule_kwargs = login_manager_mock.ScheduleShutdown.call_args
  89. assert not schedule_args
  90. assert schedule_kwargs.pop("action") == action
  91. actual_delay = schedule_kwargs.pop("time") - datetime.datetime.now()
  92. assert actual_delay.total_seconds() == pytest.approx(delay.total_seconds(), abs=0.1)
  93. assert not schedule_kwargs
  94. class DBusErrorResponseMock(jeepney.wrappers.DBusErrorResponse):
  95. # pylint: disable=missing-class-docstring,super-init-not-called
  96. def __init__(self, name: str, data: typing.Any):
  97. self.name = name
  98. self.data = data
  99. @pytest.mark.parametrize("action", ["poweroff"])
  100. @pytest.mark.parametrize(
  101. ("error_name", "error_message", "log_message"),
  102. [
  103. (
  104. "test error",
  105. "test message",
  106. "[test error] ('test message',)",
  107. ),
  108. (
  109. "org.freedesktop.DBus.Error.InteractiveAuthorizationRequired",
  110. "Interactive authentication required.",
  111. "unauthorized; missing polkit authorization rules?",
  112. ),
  113. ],
  114. )
  115. def test__schedule_shutdown_fail(
  116. caplog, action, error_name, error_message, log_message
  117. ):
  118. login_manager_mock = unittest.mock.MagicMock()
  119. login_manager_mock.ScheduleShutdown.side_effect = DBusErrorResponseMock(
  120. name=error_name,
  121. data=(error_message,),
  122. )
  123. login_manager_mock.ListInhibitors.return_value = ([],)
  124. with unittest.mock.patch(
  125. "systemctl_mqtt._dbus.login_manager.get_login_manager_proxy",
  126. return_value=login_manager_mock,
  127. ), caplog.at_level(logging.DEBUG):
  128. systemctl_mqtt._dbus.login_manager.schedule_shutdown(
  129. action=action, delay=datetime.timedelta(seconds=21)
  130. )
  131. login_manager_mock.ScheduleShutdown.assert_called_once()
  132. assert len(caplog.records) == 3
  133. assert caplog.records[0].levelno == logging.INFO
  134. assert caplog.records[0].message.startswith(f"scheduling {action} for ")
  135. assert caplog.records[1].levelno == logging.ERROR
  136. assert caplog.records[1].message == f"failed to schedule {action}: {log_message}"
  137. assert "inhibitor" in caplog.records[2].message
  138. def test_suspend(caplog):
  139. login_manager_mock = unittest.mock.MagicMock()
  140. with unittest.mock.patch(
  141. "systemctl_mqtt._dbus.login_manager.get_login_manager_proxy",
  142. return_value=login_manager_mock,
  143. ), caplog.at_level(logging.INFO):
  144. systemctl_mqtt._dbus.login_manager.suspend()
  145. login_manager_mock.Suspend.assert_called_once_with(interactive=False)
  146. assert len(caplog.records) == 1
  147. assert caplog.records[0].levelno == logging.INFO
  148. assert caplog.records[0].message == "suspending system"
  149. def test_lock_all_sessions(caplog):
  150. login_manager_mock = unittest.mock.MagicMock()
  151. with unittest.mock.patch(
  152. "systemctl_mqtt._dbus.login_manager.get_login_manager_proxy",
  153. return_value=login_manager_mock,
  154. ), caplog.at_level(logging.INFO):
  155. systemctl_mqtt._dbus.login_manager.lock_all_sessions()
  156. login_manager_mock.LockSessions.assert_called_once_with()
  157. assert len(caplog.records) == 1
  158. assert caplog.records[0].levelno == logging.INFO
  159. assert caplog.records[0].message == "instruct all sessions to activate screen locks"
  160. @pytest.mark.asyncio
  161. async def test__dbus_signal_loop():
  162. # pylint: disable=too-many-locals,too-many-arguments
  163. state_mock = unittest.mock.AsyncMock()
  164. with unittest.mock.patch(
  165. "jeepney.io.asyncio.open_dbus_router",
  166. ) as open_dbus_router_mock:
  167. async with open_dbus_router_mock() as dbus_router_mock:
  168. pass
  169. add_match_reply = unittest.mock.Mock()
  170. add_match_reply.body = ()
  171. dbus_router_mock.send_and_get_reply.return_value = add_match_reply
  172. msg_queue = asyncio.Queue()
  173. await msg_queue.put(jeepney.low_level.Message(header=None, body=(False,)))
  174. await msg_queue.put(jeepney.low_level.Message(header=None, body=(True,)))
  175. await msg_queue.put(jeepney.low_level.Message(header=None, body=(False,)))
  176. dbus_router_mock.filter = unittest.mock.MagicMock()
  177. dbus_router_mock.filter.return_value.__enter__.return_value = msg_queue
  178. # asyncio.TaskGroup added in python3.11
  179. loop_task = asyncio.create_task(
  180. systemctl_mqtt._dbus_signal_loop(
  181. state=state_mock, mqtt_client=unittest.mock.MagicMock()
  182. )
  183. )
  184. async def _abort_after_msg_queue():
  185. await msg_queue.join()
  186. loop_task.cancel()
  187. with pytest.raises(asyncio.exceptions.CancelledError):
  188. await asyncio.gather(*(loop_task, _abort_after_msg_queue()))
  189. assert unittest.mock.call(bus="SYSTEM") in open_dbus_router_mock.call_args_list
  190. dbus_router_mock.filter.assert_called_once()
  191. (filter_match_rule,) = dbus_router_mock.filter.call_args[0]
  192. assert (
  193. filter_match_rule.header_fields["interface"] == "org.freedesktop.login1.Manager"
  194. )
  195. assert filter_match_rule.header_fields["member"] == "PrepareForShutdown"
  196. add_match_msg = dbus_router_mock.send_and_get_reply.call_args[0][0]
  197. assert (
  198. add_match_msg.header.fields[jeepney.low_level.HeaderFields.member] == "AddMatch"
  199. )
  200. assert add_match_msg.body == (
  201. "interface='org.freedesktop.login1.Manager',member='PrepareForShutdown'"
  202. ",path='/org/freedesktop/login1',type='signal'",
  203. )
  204. assert [
  205. c[1]["active"] for c in state_mock.preparing_for_shutdown_handler.call_args_list
  206. ] == [False, True, False]