using System.Text.Json; using MeterVault.Infrastructure.Ingestion; namespace MeterVault.Integration.Tests.Ingestion; /// /// The pure Home Assistant WebSocket protocol helpers: URL derivation, the auth/subscribe frames, /// and reading the entity id + numeric value out of a state_changed event (the transport is /// exercised separately). No network — these are the parts that must be provably correct. /// public sealed class HaWebSocketProtocolTests { [Theory] [InlineData("http://ha.local:8123", "ws", "ha.local", 8123)] [InlineData("http://ha.local:8123/", "ws", "ha.local", 8123)] [InlineData("https://ha.example.com", "wss", "ha.example.com", 443)] public void WebSocketUri_maps_scheme_and_appends_api_path(string baseUrl, string scheme, string host, int port) { var uri = HaWebSocketProtocol.WebSocketUri(baseUrl); Assert.Equal(scheme, uri.Scheme); Assert.Equal(host, uri.Host); Assert.Equal(port, uri.Port); Assert.Equal("/api/websocket", uri.AbsolutePath); } [Fact] public void AuthMessage_carries_type_and_token() { using var doc = JsonDocument.Parse(HaWebSocketProtocol.AuthMessage("secret-token")); Assert.Equal("auth", doc.RootElement.GetProperty("type").GetString()); Assert.Equal("secret-token", doc.RootElement.GetProperty("access_token").GetString()); } [Fact] public void SubscribeStateChanged_requests_state_changed_events() { using var doc = JsonDocument.Parse(HaWebSocketProtocol.SubscribeStateChanged(7)); Assert.Equal(7, doc.RootElement.GetProperty("id").GetInt32()); Assert.Equal("subscribe_events", doc.RootElement.GetProperty("type").GetString()); Assert.Equal("state_changed", doc.RootElement.GetProperty("event_type").GetString()); } [Theory] [InlineData("{\"type\":\"auth_required\",\"ha_version\":\"2024.6\"}", true, false, false)] [InlineData("{\"type\":\"auth_ok\"}", false, true, false)] [InlineData("{\"type\":\"auth_invalid\",\"message\":\"bad token\"}", false, false, true)] public void Auth_message_types_are_recognized(string json, bool required, bool ok, bool invalid) { using var doc = JsonDocument.Parse(json); var root = doc.RootElement; Assert.Equal(required, HaWebSocketProtocol.IsAuthRequired(root)); Assert.Equal(ok, HaWebSocketProtocol.IsAuthOk(root)); Assert.Equal(invalid, HaWebSocketProtocol.IsAuthInvalid(root)); } [Fact] public void TryReadStateChanged_extracts_entity_and_numeric_state() { using var doc = JsonDocument.Parse(StateChangedEvent(state: "1234.5")); Assert.True(HaWebSocketProtocol.TryReadStateChanged(doc.RootElement, out var entityId, out var newState)); Assert.Equal("sensor.house_power", entityId); var parsed = HaStateClient.ParseStateElement(newState, attribute: null); Assert.NotNull(parsed); Assert.Equal(1234.5, parsed!.Value.Value, 3); Assert.Equal(new DateTimeOffset(2024, 6, 15, 10, 0, 0, TimeSpan.Zero), parsed.Value.Time); } [Fact] public void TryReadStateChanged_reads_a_named_attribute() { using var doc = JsonDocument.Parse(StateChangedEvent(state: "on")); Assert.True(HaWebSocketProtocol.TryReadStateChanged(doc.RootElement, out _, out var newState)); // state "on" is non-numeric, but the 'current' attribute is a number. Assert.Null(HaStateClient.ParseStateElement(newState, attribute: null)); var byAttribute = HaStateClient.ParseStateElement(newState, attribute: "current"); Assert.Equal(42, byAttribute!.Value.Value, 3); } [Fact] public void TryReadStateChanged_ignores_removed_entities_and_other_events() { using var removed = JsonDocument.Parse( "{\"type\":\"event\",\"event\":{\"event_type\":\"state_changed\",\"data\":{\"entity_id\":\"sensor.x\",\"new_state\":null}}}"); Assert.False(HaWebSocketProtocol.TryReadStateChanged(removed.RootElement, out _, out _)); using var other = JsonDocument.Parse( "{\"type\":\"event\",\"event\":{\"event_type\":\"call_service\",\"data\":{}}}"); Assert.False(HaWebSocketProtocol.TryReadStateChanged(other.RootElement, out _, out _)); using var result = JsonDocument.Parse("{\"id\":1,\"type\":\"result\",\"success\":true}"); Assert.False(HaWebSocketProtocol.TryReadStateChanged(result.RootElement, out _, out _)); } private static string StateChangedEvent(string state) => $$""" { "id": 1, "type": "event", "event": { "event_type": "state_changed", "data": { "entity_id": "sensor.house_power", "new_state": { "entity_id": "sensor.house_power", "state": "{{state}}", "attributes": { "unit_of_measurement": "W", "current": 42 }, "last_updated": "2024-06-15T10:00:00+00:00" } } } } """; }