test_state_dbus.py 6.7 KB

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