|
15 | 15 | INSTALL_PATH = os.getenv('NETALERTX_APP', '/app') |
16 | 16 | sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) |
17 | 17 |
|
18 | | -from utils.datetime_utils import timeNowUTC, DATETIME_PATTERN # noqa: E402 |
| 18 | +from utils.datetime_utils import timeNowUTC, format_date_iso, DATETIME_PATTERN # noqa: E402 |
19 | 19 |
|
20 | 20 |
|
21 | 21 | class TestTimeNowUTC: |
@@ -104,3 +104,52 @@ def test_timeNowUTC_multiple_calls_increase(self): |
104 | 104 | t2 = datetime.datetime.strptime(t2_str, DATETIME_PATTERN) |
105 | 105 |
|
106 | 106 | assert t2 >= t1 |
| 107 | + |
| 108 | + |
| 109 | +class TestFormatDateIso: |
| 110 | + """ |
| 111 | + Regression tests for format_date_iso(). |
| 112 | +
|
| 113 | + Root cause being guarded: DB timestamps are stored as naive UTC strings |
| 114 | + (e.g. '2026-04-04 08:54:00'). The old prepTimeStamp() called |
| 115 | + conf.tz.localize() which LABELS the naive value with the local TZ offset |
| 116 | + instead of CONVERTING it. This made '08:54 UTC' become '08:54+02:00', |
| 117 | + telling Home Assistant the event happened at 06:54 UTC — 2 hours too early. |
| 118 | +
|
| 119 | + format_date_iso() correctly replaces(tzinfo=UTC) first, then converts. |
| 120 | + """ |
| 121 | + |
| 122 | + def test_naive_utc_string_gets_utc_tzinfo(self): |
| 123 | + """A naive DB timestamp must be interpreted as UTC, not local time.""" |
| 124 | + result = format_date_iso("2026-04-04 08:54:00") |
| 125 | + assert result is not None |
| 126 | + # Must contain a TZ offset ('+' or 'Z'), not be naive |
| 127 | + assert "+" in result or result.endswith("Z"), \ |
| 128 | + f"Expected timezone in ISO output, got: {result}" |
| 129 | + |
| 130 | + def test_naive_utc_string_offset_reflects_utc_source(self): |
| 131 | + """ |
| 132 | + The UTC instant must be preserved. Whatever the local offset, the |
| 133 | + calendar moment encoded in the ISO string must equal 08:54 UTC. |
| 134 | + """ |
| 135 | + result = format_date_iso("2026-04-04 08:54:00") |
| 136 | + parsed = datetime.datetime.fromisoformat(result) |
| 137 | + # Normalise to UTC for the assertion |
| 138 | + utc_parsed = parsed.astimezone(datetime.UTC) |
| 139 | + assert utc_parsed.hour == 8 |
| 140 | + assert utc_parsed.minute == 54 |
| 141 | + |
| 142 | + def test_empty_string_returns_none(self): |
| 143 | + """format_date_iso('') must return None, not raise.""" |
| 144 | + assert format_date_iso("") is None |
| 145 | + |
| 146 | + def test_none_returns_none(self): |
| 147 | + """format_date_iso(None) must return None, not raise.""" |
| 148 | + assert format_date_iso(None) is None |
| 149 | + |
| 150 | + def test_output_is_valid_iso8601(self): |
| 151 | + """Output must be parseable by datetime.fromisoformat().""" |
| 152 | + result = format_date_iso("2026-01-15 12:00:00") |
| 153 | + assert result is not None |
| 154 | + # Should not raise |
| 155 | + datetime.datetime.fromisoformat(result) |
0 commit comments