|
4 | 4 |
|
5 | 5 | import base64 |
6 | 6 | import json |
| 7 | +from pathlib import Path |
7 | 8 | from unittest.mock import MagicMock, patch |
8 | 9 |
|
9 | 10 | import pytest |
@@ -65,10 +66,10 @@ def test_frozen(self): |
65 | 66 | SAMPLE_JSONL = ( |
66 | 67 | '{"device_id": "0xBB2973BD", "seq_num": 153, "device_type": "silabs", ' |
67 | 68 | '"timestamp": 1774289859.339, "rssi_dB": -42.3, "channel_num": 2, ' |
68 | | - f'"freq_offset_hz": 21654.5, "payload": "{_PAYLOAD_B64}"}}\n' |
| 69 | + f'"freq_offset_hz": 21654.5, "payload_b64": "{_PAYLOAD_B64}"}}\n' |
69 | 70 | '{"device_id": "0xBB2973BD", "seq_num": 154, "device_type": "silabs", ' |
70 | 71 | '"timestamp": 1774289863.860, "rssi_dB": -42.9, "channel_num": 15, ' |
71 | | - f'"freq_offset_hz": 21588.0, "payload": "{_PAYLOAD_B64}"}}\n' |
| 72 | + f'"freq_offset_hz": 21588.0, "payload_b64": "{_PAYLOAD_B64}"}}\n' |
72 | 73 | ) |
73 | 74 |
|
74 | 75 |
|
@@ -136,6 +137,96 @@ def test_key_tuple(self): |
136 | 137 | # --------------------------------------------------------------------------- |
137 | 138 |
|
138 | 139 |
|
| 140 | +class TestGetClient: |
| 141 | + @patch("docker.from_env") |
| 142 | + def test_from_env_succeeds(self, mock_from_env): |
| 143 | + mock_client = MagicMock() |
| 144 | + mock_from_env.return_value = mock_client |
| 145 | + assert sat._get_client() is mock_client |
| 146 | + |
| 147 | + @patch("hubblenetwork.sat._DOCKER_DESKTOP_SOCKETS", []) |
| 148 | + @patch("docker.from_env") |
| 149 | + def test_from_env_fails_no_fallbacks(self, mock_from_env): |
| 150 | + import docker |
| 151 | + |
| 152 | + mock_from_env.side_effect = docker.errors.DockerException("no sock") |
| 153 | + with pytest.raises(DockerError, match="Docker is not available"): |
| 154 | + sat._get_client() |
| 155 | + |
| 156 | + @patch("docker.DockerClient") |
| 157 | + @patch("docker.from_env") |
| 158 | + def test_fallback_to_desktop_socket(self, mock_from_env, mock_client_cls, tmp_path): |
| 159 | + import docker |
| 160 | + |
| 161 | + mock_from_env.side_effect = docker.errors.DockerException("no sock") |
| 162 | + |
| 163 | + # Create a fake socket file so the path.exists() check passes. |
| 164 | + fake_sock = tmp_path / "docker.sock" |
| 165 | + fake_sock.touch() |
| 166 | + |
| 167 | + mock_client = MagicMock() |
| 168 | + mock_client_cls.return_value = mock_client |
| 169 | + |
| 170 | + with patch("hubblenetwork.sat._DOCKER_DESKTOP_SOCKETS", [fake_sock]): |
| 171 | + result = sat._get_client() |
| 172 | + |
| 173 | + mock_client_cls.assert_called_once_with(base_url=f"unix://{fake_sock}") |
| 174 | + mock_client.ping.assert_called_once() |
| 175 | + assert result is mock_client |
| 176 | + |
| 177 | + @patch("docker.DockerClient") |
| 178 | + @patch("docker.from_env") |
| 179 | + def test_fallback_skips_nonexistent_sockets( |
| 180 | + self, mock_from_env, mock_client_cls, tmp_path |
| 181 | + ): |
| 182 | + import docker |
| 183 | + |
| 184 | + mock_from_env.side_effect = docker.errors.DockerException("no sock") |
| 185 | + |
| 186 | + missing = tmp_path / "missing.sock" |
| 187 | + existing = tmp_path / "docker.sock" |
| 188 | + existing.touch() |
| 189 | + |
| 190 | + mock_client = MagicMock() |
| 191 | + mock_client_cls.return_value = mock_client |
| 192 | + |
| 193 | + with patch( |
| 194 | + "hubblenetwork.sat._DOCKER_DESKTOP_SOCKETS", [missing, existing] |
| 195 | + ): |
| 196 | + result = sat._get_client() |
| 197 | + |
| 198 | + # Only the existing socket should be tried. |
| 199 | + mock_client_cls.assert_called_once_with(base_url=f"unix://{existing}") |
| 200 | + assert result is mock_client |
| 201 | + |
| 202 | + @patch("docker.DockerClient") |
| 203 | + @patch("docker.from_env") |
| 204 | + def test_fallback_skips_socket_that_fails_ping( |
| 205 | + self, mock_from_env, mock_client_cls, tmp_path |
| 206 | + ): |
| 207 | + import docker |
| 208 | + |
| 209 | + mock_from_env.side_effect = docker.errors.DockerException("no sock") |
| 210 | + |
| 211 | + bad_sock = tmp_path / "bad.sock" |
| 212 | + bad_sock.touch() |
| 213 | + good_sock = tmp_path / "good.sock" |
| 214 | + good_sock.touch() |
| 215 | + |
| 216 | + bad_client = MagicMock() |
| 217 | + bad_client.ping.side_effect = Exception("connection refused") |
| 218 | + good_client = MagicMock() |
| 219 | + |
| 220 | + mock_client_cls.side_effect = [bad_client, good_client] |
| 221 | + |
| 222 | + with patch( |
| 223 | + "hubblenetwork.sat._DOCKER_DESKTOP_SOCKETS", [bad_sock, good_sock] |
| 224 | + ): |
| 225 | + result = sat._get_client() |
| 226 | + |
| 227 | + assert result is good_client |
| 228 | + |
| 229 | + |
139 | 230 | class TestDockerHelpers: |
140 | 231 | @patch("hubblenetwork.sat._get_client") |
141 | 232 | def test_ensure_docker_available_success(self, mock_get_client): |
@@ -254,4 +345,4 @@ def test_docker_not_available(self, mock_sat, runner): |
254 | 345 |
|
255 | 346 | result = runner.invoke(cli, ["sat", "scan", "--timeout", "1"]) |
256 | 347 | assert result.exit_code != 0 |
257 | | - assert "Docker" in result.output |
| 348 | + assert "Docker is not installed" in result.output |
0 commit comments