using MeterVault.App; using MeterVault.Core.Domain; namespace MeterVault.Integration.Tests; /// /// Hand-entered timestamps and meter-page addresses: the pure pieces behind the reading and event /// dialogs and the deep links into a meter. No database, so these run without Docker. /// public sealed class LocalTimeEntryTests { private static readonly TimeZoneInfo Berlin = LocalTimeEntry.Resolve("Europe/Berlin"); [Fact] public void Wall_clock_time_is_read_in_the_instance_timezone() { var entry = new LocalTimeEntry(Berlin) { Date = new DateTime(2026, 9, 17), TimeOfDay = new TimeSpan(9, 30, 0) }; Assert.Equal(new DateTimeOffset(2026, 9, 17, 7, 30, 0, TimeSpan.Zero), entry.Utc); // CEST = UTC+2 } [Fact] public void A_spring_forward_time_names_no_instant() { var entry = new LocalTimeEntry(Berlin) { Date = new DateTime(2026, 3, 29), TimeOfDay = new TimeSpan(2, 30, 0) }; Assert.True(entry.IsSkipped); Assert.Null(entry.Utc); } [Fact] public void An_ambiguous_autumn_time_resolves_to_standard_time() { var entry = new LocalTimeEntry(Berlin) { Date = new DateTime(2026, 10, 25), TimeOfDay = new TimeSpan(2, 30, 0) }; Assert.Equal(new DateTimeOffset(2026, 10, 25, 1, 30, 0, TimeSpan.Zero), entry.Utc); // CET = UTC+1 } [Fact] public void Setting_an_instant_round_trips_to_the_minute() { var entry = new LocalTimeEntry(Berlin); entry.Set(new DateTimeOffset(2026, 9, 17, 7, 30, 42, TimeSpan.Zero)); Assert.Equal(new DateTime(2026, 9, 17), entry.Date); Assert.Equal(new TimeSpan(9, 30, 0), entry.TimeOfDay); Assert.Equal(new DateTimeOffset(2026, 9, 17, 7, 30, 0, TimeSpan.Zero), entry.Utc); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("Not/AZone")] public void An_unknown_timezone_falls_back_to_utc(string? id) => Assert.Equal(TimeZoneInfo.Utc, LocalTimeEntry.Resolve(id)); [Fact] public void Meter_links_address_a_tab_and_an_action() { Assert.Equal("/meters/7", MeterLinks.Detail(7)); Assert.Equal("/meters/7?tab=events&action=swap", MeterLinks.Event(7, MeterEventType.MeterSwap)); Assert.Equal("/meters/7?tab=readings&action=reading", MeterLinks.QuickEntry(7, MeterMode.CumulativeCounter)); Assert.Equal("/meters/7?tab=events&action=tank-level", MeterLinks.QuickEntry(7, MeterMode.ConsumableBalance)); Assert.Null(MeterLinks.QuickEntry(7, MeterMode.Virtual)); } [Fact] public void Source_and_connector_links_carry_the_way_back() { Assert.Equal("/meters/7?tab=sources&action=source", MeterLinks.Source(7)); Assert.Equal( "/meters/7?tab=sources&action=source&source=3&type=Tasmota&connector=12", MeterLinks.Source(7, sourceId: 3, sourceType: SourceType.Tasmota, connectorId: 12)); Assert.Equal( "/admin/connectors?new=MqttBroker&meter=7&type=Tasmota", MeterLinks.NewConnector(7, sourceId: null, SourceType.Tasmota, EndpointType.MqttBroker)); Assert.Equal( "/admin/connectors?edit=12&meter=7&source=3&type=HomeAssistant", MeterLinks.EditConnector(7, sourceId: 3, SourceType.HomeAssistant, connectorId: 12)); } [Fact] public void Every_event_type_round_trips_through_its_action() { foreach (var type in Enum.GetValues()) { Assert.Equal(type, MeterLinks.EventFor(MeterLinks.ActionFor(type))); } Assert.Null(MeterLinks.EventFor(MeterLinks.ActionReading)); Assert.Null(MeterLinks.EventFor(null)); } [Theory] [InlineData(null, 0)] [InlineData("readings", 0)] [InlineData("EVENTS", 2)] [InlineData("sources", 4)] [InlineData("nonsense", 0)] public void Tab_keys_map_to_panel_indexes(string? tab, int expected) => Assert.Equal(expected, MeterLinks.TabIndex(tab)); }