Windfree poller application & jenkins pipeline
This commit is contained in:
80
tests/test_config.py
Normal file
80
tests/test_config.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from windfree_poller.config import load_config
|
||||
|
||||
|
||||
class ConfigTests(unittest.TestCase):
|
||||
def test_environment_secrets_override_toml(self):
|
||||
text = '''
|
||||
[poller]
|
||||
collector_id="pi"
|
||||
device_id="id"
|
||||
[smartthings]
|
||||
access_token="file-token"
|
||||
[redpanda_connect]
|
||||
url="http://localhost/windfree"
|
||||
[[fields]]
|
||||
name="temperature"
|
||||
component="main"
|
||||
capability="temperatureMeasurement"
|
||||
attribute="temperature"
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / "config.toml"
|
||||
path.write_text(text, encoding="utf-8")
|
||||
with patch.dict(os.environ, {"SMARTTHINGS_ACCESS_TOKEN": "env-token"}):
|
||||
self.assertEqual(load_config(path).access_token, "env-token")
|
||||
|
||||
def test_access_token_can_be_read_from_file(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
token_path = Path(folder) / "token"
|
||||
token_path.write_text("file-token\n", encoding="utf-8")
|
||||
config_path = Path(folder) / "config.toml"
|
||||
config_path.write_text(f'''
|
||||
[poller]
|
||||
collector_id="pi"
|
||||
device_id="id"
|
||||
[smartthings]
|
||||
access_token_file="{token_path.as_posix()}"
|
||||
[redpanda_connect]
|
||||
url="http://localhost/windfree"
|
||||
[[fields]]
|
||||
name="temperature"
|
||||
component="main"
|
||||
capability="temperatureMeasurement"
|
||||
attribute="temperature"
|
||||
''', encoding="utf-8")
|
||||
self.assertEqual(load_config(config_path).access_token, "file-token")
|
||||
|
||||
def test_saved_oauth_token_state_is_accepted(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
token_path = Path(folder) / "token.json"
|
||||
token_path.write_text('{"access_token":"access","refresh_token":"refresh"}', encoding="utf-8")
|
||||
config_path = Path(folder) / "config.toml"
|
||||
config_path.write_text(f'''
|
||||
[poller]
|
||||
collector_id="pi"
|
||||
device_id="id"
|
||||
[smartthings]
|
||||
client_id="client"
|
||||
client_secret="secret"
|
||||
token_file="{token_path.as_posix()}"
|
||||
[redpanda_connect]
|
||||
url="http://localhost/windfree"
|
||||
[[fields]]
|
||||
name="temperature"
|
||||
component="main"
|
||||
capability="temperatureMeasurement"
|
||||
attribute="temperature"
|
||||
''', encoding="utf-8")
|
||||
loaded = load_config(config_path)
|
||||
self.assertEqual(loaded.client_id, "client")
|
||||
self.assertEqual(loaded.access_token, "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
38
tests/test_events.py
Normal file
38
tests/test_events.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from windfree_poller.config import Config, Field
|
||||
from windfree_poller.events import extract, status_event
|
||||
|
||||
|
||||
def config(*fields):
|
||||
return Config("pi", "device", 300, 10, "https://example/v1", "", "", "", "token", Path("token.json"), "http://localhost", 10, fields)
|
||||
|
||||
|
||||
class EventTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.status = {"components": {"main": {
|
||||
"temperatureMeasurement": {"temperature": {"value": 23.5, "unit": "C", "timestamp": "2026-01-01T00:00:00Z"}},
|
||||
"airConditionerFanMode": {"fanMode": {"value": "windFree"}},
|
||||
}}}
|
||||
|
||||
def test_extract_and_equals_conversion(self):
|
||||
found, value, _, _ = extract(self.status, Field("windfree", "main", "airConditionerFanMode", "fanMode", "windFree"))
|
||||
self.assertTrue(found)
|
||||
self.assertIs(value, True)
|
||||
|
||||
def test_missing_field_is_warning(self):
|
||||
event = status_event(config(Field("temperature", "main", "temperatureMeasurement", "temperature"),
|
||||
Field("humidity", "main", "relativeHumidityMeasurement", "humidity")), self.status)
|
||||
self.assertEqual(event["status"], "no_data")
|
||||
self.assertEqual(event["level"], "warning")
|
||||
self.assertEqual(event["missing_fields"], ["humidity"])
|
||||
|
||||
def test_all_fields_is_ok(self):
|
||||
event = status_event(config(Field("temperature", "main", "temperatureMeasurement", "temperature")), self.status)
|
||||
self.assertEqual((event["level"], event["status"]), ("info", "ok"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
52
tests/test_oauth.py
Normal file
52
tests/test_oauth.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from windfree_poller.oauth_server import State, save_tokens, token_metadata
|
||||
from windfree_poller.oauth_config import OAuthConfig
|
||||
|
||||
|
||||
def config(path: Path) -> OAuthConfig:
|
||||
return OAuthConfig(
|
||||
"127.0.0.1", 8088, "https://example.com",
|
||||
"https://sso.example/realms/test", "client", "secret", "admin",
|
||||
"https://example.com/oauth/keycloak/callback", "st-client", "st-secret",
|
||||
"https://example.com/oauth/smartthings/callback", ("r:devices:$",),
|
||||
"x" * 32, path,
|
||||
)
|
||||
|
||||
|
||||
class OAuthTests(unittest.TestCase):
|
||||
def test_signed_session_cookie_cannot_be_modified(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
state = State(config(Path(folder) / "token.json"))
|
||||
session_id, session = state.new_session()
|
||||
self.assertIs(state.verify_cookie(state.cookie_value(session_id))[1], session)
|
||||
self.assertIsNone(state.verify_cookie(state.cookie_value(session_id) + "changed"))
|
||||
|
||||
def test_tokens_are_saved_atomically_without_exposing_them_in_metadata(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / "token.json"
|
||||
save_tokens(path, {
|
||||
"access_token": "access", "refresh_token": "refresh",
|
||||
"expires_in": 60, "installed_app_id": "installed", "scope": "r:devices:$",
|
||||
})
|
||||
saved = json.loads(path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(saved["refresh_token"], "refresh")
|
||||
self.assertGreater(saved["expires_at"], time.time())
|
||||
metadata = token_metadata(path)
|
||||
self.assertTrue(metadata["connected"])
|
||||
self.assertNotIn("access_token", metadata)
|
||||
self.assertNotIn("refresh_token", metadata)
|
||||
if os.name != "nt":
|
||||
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
|
||||
|
||||
def test_missing_token_file_is_disconnected(self):
|
||||
self.assertEqual(token_metadata(Path("missing-token-file")), {"connected": False})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user