|
16 | 16 |
|
17 | 17 | import pytest |
18 | 18 | from awslabs.billing_cost_management_mcp_server.utilities.time_utils import ( |
| 19 | + EPOCH_MILLISECONDS_THRESHOLD, |
| 20 | + format_timestamp_to_utc_iso, |
19 | 21 | normalize_datetimes_to_iso, |
20 | 22 | timestamp_to_utc_iso_string, |
21 | 23 | ) |
22 | | -from datetime import datetime, timezone |
| 24 | +from datetime import datetime, timedelta, timezone |
| 25 | +from unittest.mock import patch |
23 | 26 |
|
24 | 27 |
|
25 | 28 | class TestUtcDatetimeStringToEpochSeconds: |
@@ -153,3 +156,68 @@ def test_scalar_passthrough(self): |
153 | 156 | assert normalize_datetimes_to_iso('hello') == 'hello' |
154 | 157 | assert normalize_datetimes_to_iso(42) == 42 |
155 | 158 | assert normalize_datetimes_to_iso(None) is None |
| 159 | + |
| 160 | + |
| 161 | +class TestFormatTimestampToUtcIso: |
| 162 | + """Tests for format_timestamp_to_utc_iso (datetime, epoch seconds, epoch ms).""" |
| 163 | + |
| 164 | + # 2021-09-28T10:40:00Z in each wire form. |
| 165 | + EXPECTED = '2021-09-28T10:40:00' |
| 166 | + EPOCH_SECONDS = 1632825600 |
| 167 | + EPOCH_MS = 1632825600000 |
| 168 | + |
| 169 | + def test_none_returns_none(self): |
| 170 | + """A missing timestamp is None, not an error string.""" |
| 171 | + assert format_timestamp_to_utc_iso(None) is None |
| 172 | + |
| 173 | + def test_naive_datetime_is_taken_as_utc(self): |
| 174 | + """boto3-style naive datetime is formatted as-is (UTC).""" |
| 175 | + assert format_timestamp_to_utc_iso(datetime(2021, 9, 28, 10, 40)) == self.EXPECTED |
| 176 | + |
| 177 | + def test_aware_datetime_is_converted_to_utc(self): |
| 178 | + """An aware datetime in another zone is converted to UTC.""" |
| 179 | + eastern = timezone(timedelta(hours=-4)) |
| 180 | + value = datetime(2021, 9, 28, 6, 40, tzinfo=eastern) |
| 181 | + assert format_timestamp_to_utc_iso(value) == self.EXPECTED |
| 182 | + |
| 183 | + def test_epoch_seconds(self): |
| 184 | + """Epoch seconds (the AWS JSON wire form) are not divided by 1000.""" |
| 185 | + assert format_timestamp_to_utc_iso(self.EPOCH_SECONDS) == self.EXPECTED |
| 186 | + |
| 187 | + def test_epoch_seconds_float(self): |
| 188 | + """Fractional epoch seconds keep their sub-second part.""" |
| 189 | + assert format_timestamp_to_utc_iso(1632825600.5) == '2021-09-28T10:40:00.500000' |
| 190 | + |
| 191 | + def test_epoch_milliseconds(self): |
| 192 | + """Epoch milliseconds are recognized by magnitude.""" |
| 193 | + assert format_timestamp_to_utc_iso(self.EPOCH_MS) == self.EXPECTED |
| 194 | + |
| 195 | + def test_threshold_boundary(self): |
| 196 | + """Values at the threshold are seconds; values above are milliseconds.""" |
| 197 | + at = format_timestamp_to_utc_iso(EPOCH_MILLISECONDS_THRESHOLD) |
| 198 | + assert at is not None and at.startswith('5138-') |
| 199 | + above = format_timestamp_to_utc_iso(EPOCH_MILLISECONDS_THRESHOLD + 1) |
| 200 | + assert above is not None and above.startswith('1973-') |
| 201 | + |
| 202 | + def test_epoch_zero_is_a_value(self): |
| 203 | + """Epoch 0 is a real instant, not a missing value.""" |
| 204 | + assert format_timestamp_to_utc_iso(0) == '1970-01-01T00:00:00' |
| 205 | + |
| 206 | + @pytest.mark.parametrize( |
| 207 | + 'value', |
| 208 | + ['1632825600', '1632825600000', '2021-09-28T10:40:00Z', '2021-09-28T10:40:00+00:00'], |
| 209 | + ) |
| 210 | + def test_strings(self, value): |
| 211 | + """Numeric and ISO 8601 strings are accepted.""" |
| 212 | + assert format_timestamp_to_utc_iso(value) == self.EXPECTED |
| 213 | + |
| 214 | + @pytest.mark.parametrize( |
| 215 | + 'value', ['not a timestamp', '', True, float('nan'), float('inf'), 1e20, [1], {}] |
| 216 | + ) |
| 217 | + def test_unconvertible_returns_none_and_logs(self, value): |
| 218 | + """Unconvertible input returns None and logs; no error text in the value.""" |
| 219 | + with patch( |
| 220 | + 'awslabs.billing_cost_management_mcp_server.utilities.time_utils.logger' |
| 221 | + ) as mock_logger: |
| 222 | + assert format_timestamp_to_utc_iso(value) is None |
| 223 | + mock_logger.warning.assert_called_once() |
0 commit comments