test_state_dbus.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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 datetime
  18. import json
  19. import logging
  20. import re
  21. import unittest.mock
  22. import dbus.types
  23. import pytest
  24. import systemctl_mqtt
  25. # pylint: disable=protected-access
  26. def test_shutdown_lock():
  27. lock_fd = unittest.mock.MagicMock()
  28. with unittest.mock.patch("systemctl_mqtt._dbus.get_login_manager"):
  29. state = systemctl_mqtt._State(
  30. mqtt_topic_prefix="any",
  31. homeassistant_discovery_prefix=None,
  32. homeassistant_discovery_object_id=None,
  33. poweroff_delay=datetime.timedelta(),
  34. )
  35. state._login_manager.Inhibit.return_value = lock_fd
  36. state.acquire_shutdown_lock()
  37. state._login_manager.Inhibit.assert_called_once_with(
  38. "shutdown", "systemctl-mqtt", "Report shutdown via MQTT", "delay"
  39. )
  40. assert state._shutdown_lock == lock_fd
  41. # https://dbus.freedesktop.org/doc/dbus-python/dbus.types.html#dbus.types.UnixFd.take
  42. lock_fd.take.return_value = "fdnum"
  43. with unittest.mock.patch("os.close") as close_mock:
  44. state.release_shutdown_lock()
  45. close_mock.assert_called_once_with("fdnum")
  46. @pytest.mark.parametrize("active", [True, False])
  47. def test_prepare_for_shutdown_handler(caplog, active):
  48. with unittest.mock.patch("systemctl_mqtt._dbus.get_login_manager"):
  49. state = systemctl_mqtt._State(
  50. mqtt_topic_prefix="any",
  51. homeassistant_discovery_prefix=None,
  52. homeassistant_discovery_object_id=None,
  53. poweroff_delay=datetime.timedelta(),
  54. )
  55. mqtt_client_mock = unittest.mock.MagicMock()
  56. state.register_prepare_for_shutdown_handler(mqtt_client=mqtt_client_mock)
  57. # pylint: disable=no-member,comparison-with-callable
  58. connect_to_signal_kwargs = state._login_manager.connect_to_signal.call_args[1]
  59. assert connect_to_signal_kwargs["signal_name"] == "PrepareForShutdown"
  60. handler_function = connect_to_signal_kwargs["handler_function"]
  61. assert handler_function.func == state._prepare_for_shutdown_handler
  62. with unittest.mock.patch.object(
  63. state, "acquire_shutdown_lock"
  64. ) as acquire_lock_mock, unittest.mock.patch.object(
  65. state, "release_shutdown_lock"
  66. ) as release_lock_mock:
  67. handler_function(dbus.types.Boolean(active))
  68. if active:
  69. acquire_lock_mock.assert_not_called()
  70. release_lock_mock.assert_called_once_with()
  71. else:
  72. acquire_lock_mock.assert_called_once_with()
  73. release_lock_mock.assert_not_called()
  74. mqtt_client_mock.publish.assert_called_once_with(
  75. topic="any/preparing-for-shutdown",
  76. payload="true" if active else "false",
  77. retain=True,
  78. )
  79. assert len(caplog.records) == 1
  80. assert caplog.records[0].levelno == logging.ERROR
  81. assert caplog.records[0].message.startswith(
  82. "failed to publish on any/preparing-for-shutdown"
  83. )
  84. @pytest.mark.parametrize("active", [True, False])
  85. def test_publish_preparing_for_shutdown(active):
  86. login_manager_mock = unittest.mock.MagicMock()
  87. login_manager_mock.Get.return_value = dbus.Boolean(active)
  88. with unittest.mock.patch(
  89. "systemctl_mqtt._dbus.get_login_manager", return_value=login_manager_mock
  90. ):
  91. state = systemctl_mqtt._State(
  92. mqtt_topic_prefix="any",
  93. homeassistant_discovery_prefix=None,
  94. homeassistant_discovery_object_id=None,
  95. poweroff_delay=datetime.timedelta(),
  96. )
  97. assert state._login_manager == login_manager_mock
  98. mqtt_client_mock = unittest.mock.MagicMock()
  99. state.publish_preparing_for_shutdown(mqtt_client=mqtt_client_mock)
  100. login_manager_mock.Get.assert_called_once_with(
  101. "org.freedesktop.login1.Manager",
  102. "PreparingForShutdown",
  103. dbus_interface="org.freedesktop.DBus.Properties",
  104. )
  105. mqtt_client_mock.publish.assert_called_once_with(
  106. topic="any/preparing-for-shutdown",
  107. payload="true" if active else "false",
  108. retain=True,
  109. )
  110. def test_publish_preparing_for_shutdown_get_fail(caplog):
  111. login_manager_mock = unittest.mock.MagicMock()
  112. login_manager_mock.Get.side_effect = dbus.DBusException("mocked")
  113. with unittest.mock.patch(
  114. "systemctl_mqtt._dbus.get_login_manager", return_value=login_manager_mock
  115. ):
  116. state = systemctl_mqtt._State(
  117. mqtt_topic_prefix="any",
  118. homeassistant_discovery_prefix=None,
  119. homeassistant_discovery_object_id=None,
  120. poweroff_delay=datetime.timedelta(),
  121. )
  122. mqtt_client_mock = unittest.mock.MagicMock()
  123. state.publish_preparing_for_shutdown(mqtt_client=None)
  124. mqtt_client_mock.publish.assert_not_called()
  125. assert len(caplog.records) == 1
  126. assert caplog.records[0].levelno == logging.ERROR
  127. assert (
  128. caplog.records[0].message
  129. == "failed to read logind's PreparingForShutdown property: mocked"
  130. )
  131. @pytest.mark.parametrize("topic_prefix", ["systemctl/hostname", "hostname/systemctl"])
  132. @pytest.mark.parametrize("discovery_prefix", ["homeassistant", "home/assistant"])
  133. @pytest.mark.parametrize("object_id", ["raspberrypi", "debian21"])
  134. @pytest.mark.parametrize("hostname", ["hostname", "host-name"])
  135. def test_publish_homeassistant_device_config(
  136. topic_prefix, discovery_prefix, object_id, hostname
  137. ):
  138. state = systemctl_mqtt._State(
  139. mqtt_topic_prefix=topic_prefix,
  140. homeassistant_discovery_prefix=discovery_prefix,
  141. homeassistant_discovery_object_id=object_id,
  142. poweroff_delay=datetime.timedelta(),
  143. )
  144. mqtt_client = unittest.mock.MagicMock()
  145. with unittest.mock.patch(
  146. "systemctl_mqtt._utils.get_hostname", return_value=hostname
  147. ):
  148. state.publish_homeassistant_device_config(mqtt_client=mqtt_client)
  149. mqtt_client.publish.assert_called_once()
  150. publish_args, publish_kwargs = mqtt_client.publish.call_args
  151. assert not publish_args
  152. assert not publish_kwargs["retain"]
  153. assert (
  154. publish_kwargs["topic"] == discovery_prefix + "/device/" + object_id + "/config"
  155. )
  156. config = json.loads(publish_kwargs["payload"])
  157. assert re.match(r"\d+\.\d+\.", config["origin"].pop("sw_version"))
  158. assert config == {
  159. "origin": {
  160. "name": "systemctl-mqtt",
  161. "support_url": "https://github.com/fphammerle/systemctl-mqtt",
  162. },
  163. "device": {"identifiers": [hostname], "name": hostname},
  164. "components": {
  165. "logind/preparing-for-shutdown": {
  166. "unique_id": f"systemctl-mqtt-{hostname}-logind-preparing-for-shutdown",
  167. "object_id": f"{hostname}_logind_preparing_for_shutdown",
  168. "name": "preparing for shutdown",
  169. "platform": "binary_sensor",
  170. "state_topic": topic_prefix + "/preparing-for-shutdown",
  171. "payload_on": "true",
  172. "payload_off": "false",
  173. },
  174. "logind/poweroff": {
  175. "unique_id": f"systemctl-mqtt-{hostname}-logind-poweroff",
  176. "object_id": f"{hostname}_logind_poweroff",
  177. "name": "poweroff",
  178. "platform": "button",
  179. "command_topic": f"{topic_prefix}/poweroff",
  180. },
  181. "logind/lock-all-sessions": {
  182. "unique_id": f"systemctl-mqtt-{hostname}-logind-lock-all-sessions",
  183. "object_id": f"{hostname}_logind_lock_all_sessions",
  184. "name": "lock all sessions",
  185. "platform": "button",
  186. "command_topic": f"{topic_prefix}/lock-all-sessions",
  187. },
  188. },
  189. }