- add multi-device portal device management, key rotation, and sync request APIs - add Pi settings UI for remote server URL, device name, and API key - add local test-connection and sync-now actions - make upload and config sync reuse dynamic shared credentials - add Phase 25.5c migration and docs updates Co-authored-by: Copilot <[email protected]>
160 lines
5.4 KiB
Python
160 lines
5.4 KiB
Python
import os
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from modules import app_device_config, app_device_sync, app_remote_config
|
|
|
|
|
|
class RemoteConfigTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmpdir = tempfile.TemporaryDirectory()
|
|
self.creds_path = os.path.join(self.tmpdir.name, "upload_creds.env")
|
|
|
|
def tearDown(self):
|
|
self.tmpdir.cleanup()
|
|
|
|
def test_update_and_read_remote_config(self):
|
|
with patch.object(app_remote_config, "_CREDS_PATH", self.creds_path):
|
|
errors = {}
|
|
app_remote_config.update_remote_config(
|
|
{
|
|
"server_url": "https://example.test/",
|
|
"device_name": "yaris-2013",
|
|
"api_key": "secret-key",
|
|
},
|
|
errors,
|
|
)
|
|
|
|
self.assertEqual(errors, {})
|
|
config = app_remote_config.get_remote_config()
|
|
self.assertEqual(config["server_url"], "https://example.test")
|
|
self.assertEqual(config["device_name"], "yaris-2013")
|
|
self.assertTrue(config["api_key_configured"])
|
|
|
|
def test_blank_api_key_preserves_existing_until_cleared(self):
|
|
with patch.object(app_remote_config, "_CREDS_PATH", self.creds_path):
|
|
app_remote_config.update_remote_config(
|
|
{
|
|
"server_url": "https://example.test",
|
|
"device_name": "yaris-2013",
|
|
"api_key": "secret-key",
|
|
},
|
|
{},
|
|
)
|
|
|
|
errors = {}
|
|
app_remote_config.update_remote_config(
|
|
{
|
|
"server_url": "https://example.test",
|
|
"device_name": "yaris-2013",
|
|
"api_key": "",
|
|
"clear_api_key": False,
|
|
},
|
|
errors,
|
|
)
|
|
self.assertEqual(errors, {})
|
|
self.assertTrue(app_remote_config.get_remote_config()["api_key_configured"])
|
|
|
|
app_remote_config.update_remote_config(
|
|
{"clear_api_key": True},
|
|
{},
|
|
)
|
|
self.assertFalse(app_remote_config.get_remote_config()["api_key_configured"])
|
|
|
|
@patch("modules.app_remote_config.requests.get")
|
|
def test_test_connection_checks_health_and_device_auth(self, requests_get):
|
|
class Response:
|
|
def __init__(self, status_code):
|
|
self.status_code = status_code
|
|
|
|
def raise_for_status(self):
|
|
if self.status_code >= 400:
|
|
raise RuntimeError("bad status")
|
|
|
|
requests_get.side_effect = [Response(200), Response(200)]
|
|
with patch.object(app_remote_config, "_CREDS_PATH", self.creds_path):
|
|
app_remote_config.update_remote_config(
|
|
{
|
|
"server_url": "https://example.test",
|
|
"device_name": "yaris-2013",
|
|
"api_key": "secret-key",
|
|
},
|
|
{},
|
|
)
|
|
|
|
result = app_remote_config.test_connection()
|
|
|
|
self.assertTrue(result["ok"])
|
|
self.assertTrue(result["authenticated"])
|
|
self.assertEqual(requests_get.call_count, 2)
|
|
|
|
|
|
class DeviceConfigSyncPrereqTest(unittest.TestCase):
|
|
@patch("modules.app_remote_config.get_remote_config")
|
|
def test_sync_enable_requires_remote_credentials(self, get_remote_config):
|
|
get_remote_config.return_value = {
|
|
"server_url": "",
|
|
"device_name": "",
|
|
"api_key_configured": False,
|
|
}
|
|
errors = {}
|
|
|
|
with patch("modules.app_device_config._get_sync_enabled", return_value=False):
|
|
app_device_config.update_device_config({"sync_enabled": True}, errors)
|
|
|
|
self.assertIn("sync_enabled", errors)
|
|
|
|
|
|
class DeviceSyncNowTest(unittest.TestCase):
|
|
@patch("modules.app_device_sync._has_internet", return_value=True)
|
|
@patch("modules.app_device_sync._acknowledge_sync")
|
|
@patch("modules.app_device_sync._on_config_changed")
|
|
@patch("modules.app_device_sync._store_synced_config")
|
|
@patch("modules.app_device_sync.requests.get")
|
|
@patch("modules.app_remote_config.server_credentials")
|
|
def test_sync_now_fetches_and_applies_config(
|
|
self,
|
|
server_credentials,
|
|
requests_get,
|
|
store_synced_config,
|
|
on_config_changed,
|
|
acknowledge_sync,
|
|
_has_internet,
|
|
):
|
|
class Response:
|
|
status_code = 200
|
|
|
|
def raise_for_status(self):
|
|
return None
|
|
|
|
def json(self):
|
|
return {"owner_name": "alice", "config_version": 4}
|
|
|
|
server_credentials.return_value = {
|
|
"server_url": "https://example.test",
|
|
"device_name": "yaris-2013",
|
|
"api_key": "secret-key",
|
|
}
|
|
requests_get.return_value = Response()
|
|
original_version = app_device_sync._SYNC_CONFIG_VERSION
|
|
app_device_sync._SYNC_CONFIG_VERSION = 0
|
|
try:
|
|
result = app_device_sync.sync_now()
|
|
finally:
|
|
app_device_sync._SYNC_CONFIG_VERSION = original_version
|
|
|
|
self.assertEqual(result["status"], "synced")
|
|
store_synced_config.assert_called_once()
|
|
on_config_changed.assert_called_once()
|
|
acknowledge_sync.assert_called_once_with(
|
|
"https://example.test",
|
|
"yaris-2013",
|
|
"secret-key",
|
|
4,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|