Skip to content

Commit 000b58e

Browse files
test: add edge case tests for parse_to_utc in time_utils
1 parent ef4efd3 commit 000b58e

1 file changed

Lines changed: 189 additions & 0 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"""
2+
Unit tests for parse_to_utc edge cases in time_utils.
3+
4+
Covers: invalid strings, ambiguous formats, timezone edge cases, non-UTC offsets,
5+
and inputs that previously caused crashes or wrong results.
6+
"""
7+
8+
import pytest
9+
from datetime import datetime, timezone
10+
from backend.secuscan.time_utils import parse_to_utc
11+
12+
13+
class TestParseToUtcEdgeCases:
14+
"""Edge case tests for parse_to_utc."""
15+
16+
# ── None / empty / non-string input ───────────────────────────────────────
17+
18+
def test_none_returns_none(self):
19+
"""None is a no-op that returns None."""
20+
assert parse_to_utc(None) is None
21+
22+
def test_empty_string_returns_none(self):
23+
"""Empty string returns None."""
24+
assert parse_to_utc("") is None
25+
26+
def test_whitespace_only_returns_none(self):
27+
"""Whitespace-only string returns None."""
28+
assert parse_to_utc(" ") is None
29+
30+
# ── Unparseable strings → None ───────────────────────────────────────────
31+
32+
def test_broken_iso_format_returns_none(self):
33+
"""Garbage string returns None."""
34+
assert parse_to_utc("not-a-date") is None
35+
36+
def test_incomplete_iso_returns_none(self):
37+
"""Incomplete ISO fragment returns None."""
38+
assert parse_to_utc("2024-01-01T") is None
39+
40+
def test_pure_time_string_returns_none(self):
41+
"""Time-only string is not a full date."""
42+
assert parse_to_utc("12:34:56") is None
43+
44+
def test_us_date_format_returns_none(self):
45+
"""US MM/DD/YYYY is not ISO – returns None."""
46+
assert parse_to_utc("01/15/2024") is None
47+
48+
def test_eu_date_format_returns_none(self):
49+
"""EU DD/MM/YYYY is not ISO – returns None."""
50+
assert parse_to_utc("15/01/2024") is None
51+
52+
def test_slash_datetime_returns_none(self):
53+
"""Slash-separated datetime is not ISO."""
54+
assert parse_to_utc("2024/01/15 12:00:00") is None
55+
56+
def test_dot_separated_returns_none(self):
57+
"""Dot-separated datetime is not ISO."""
58+
assert parse_to_utc("2024.01.15T12:00:00") is None
59+
60+
# ── Date-only is NOT parseable ──────────────────────────────────────────
61+
62+
def test_dash_date_only_parsed_as_midnight_utc(self):
63+
"""YYYY-MM-DD (date-only) is parsed as midnight UTC."""
64+
result = parse_to_utc("2024-01-15")
65+
assert result == datetime(2024, 1, 15, 0, 0, 0, tzinfo=timezone.utc)
66+
67+
# ── Unix timestamp (int / float) ─────────────────────────────────────────
68+
69+
def test_unix_timestamp_int(self):
70+
"""Integer is interpreted as Unix epoch seconds."""
71+
result = parse_to_utc(1704067200)
72+
assert result == datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
73+
74+
def test_unix_timestamp_float(self):
75+
"""Float is interpreted as Unix epoch with sub-second precision."""
76+
result = parse_to_utc(1704067200.123)
77+
assert result.microsecond == 123000
78+
79+
# ── "now" special keyword ─────────────────────────────────────────────────
80+
81+
def test_now_returns_current_utc(self):
82+
"""The literal 'now' returns a UTC-aware current time."""
83+
import time
84+
before = datetime.now(timezone.utc)
85+
time.sleep(0.01)
86+
result = parse_to_utc("now")
87+
time.sleep(0.01)
88+
after = datetime.now(timezone.utc)
89+
assert before <= result <= after
90+
assert result.tzinfo == timezone.utc
91+
92+
# ── Space separator (SQLite convention) ──────────────────────────────────
93+
94+
def test_space_separator_converted_to_T(self):
95+
"""Space-separated ISO (SQLite format) is handled."""
96+
result = parse_to_utc("2024-06-15 14:30:00")
97+
assert result == datetime(2024, 6, 15, 14, 30, 0, tzinfo=timezone.utc)
98+
99+
# ── Z suffix ─────────────────────────────────────────────────────────────
100+
101+
def test_z_suffix_handled(self):
102+
"""Z suffix is accepted and converted to +00:00."""
103+
result = parse_to_utc("2024-01-01T00:00:00Z")
104+
assert result == datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
105+
106+
# ── Sub-second precision ──────────────────────────────────────────────────
107+
108+
def test_microseconds_preserved(self):
109+
"""Sub-second precision is preserved."""
110+
result = parse_to_utc("2024-06-15T14:30:00.123456")
111+
assert result == datetime(2024, 6, 15, 14, 30, 0, 123456, tzinfo=timezone.utc)
112+
113+
def test_subseconds_rounded_to_microseconds(self):
114+
"""Nano-second precision is truncated to microseconds."""
115+
result = parse_to_utc("2024-06-15T14:30:00.123456789")
116+
assert result.microsecond == 123456
117+
118+
# ── UTC offset handling ──────────────────────────────────────────────────
119+
120+
def test_positive_offset_converted_to_utc(self):
121+
"""Positive UTC+N offset is converted to UTC."""
122+
result = parse_to_utc("2024-01-01T03:00:00+03:00")
123+
assert result == datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
124+
125+
def test_negative_offset_converted_to_utc(self):
126+
"""Negative UTC-N offset is converted to UTC."""
127+
result = parse_to_utc("2024-01-01T03:00:00-05:00")
128+
assert result == datetime(2024, 1, 1, 8, 0, 0, tzinfo=timezone.utc)
129+
130+
def test_offset_without_colon(self):
131+
"""Offset without colon separator: +0300."""
132+
result = parse_to_utc("2024-01-01T03:00:00+0300")
133+
assert result == datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
134+
135+
def test_india_offset(self):
136+
"""India +05:30 is correctly converted."""
137+
result = parse_to_utc("2024-01-01T09:30:00+05:30")
138+
assert result == datetime(2024, 1, 1, 4, 0, 0, tzinfo=timezone.utc)
139+
140+
def test_nz_negative_fractional_offset(self):
141+
"""NZ -08:30 (Chatham Islands) is correctly converted."""
142+
result = parse_to_utc("2024-01-01T12:00:00-08:30")
143+
assert result == datetime(2024, 1, 1, 20, 30, 0, tzinfo=timezone.utc)
144+
145+
# ── Leap second ─────────────────────────────────────────────────────────
146+
147+
def test_leap_second_returns_none(self):
148+
"""Leap second (60s) is not supported – returns None."""
149+
assert parse_to_utc("2024-01-01T00:00:60Z") is None
150+
151+
# ── Year boundary ────────────────────────────────────────────────────────
152+
153+
def test_year_boundary_crossing(self):
154+
"""Year rollover at UTC midnight is preserved."""
155+
result = parse_to_utc("2023-12-31T23:59:59Z")
156+
assert result.year == 2023
157+
assert result.month == 12
158+
assert result.day == 31
159+
assert result.hour == 23
160+
161+
# ── Naive datetime (no tzinfo) ──────────────────────────────────────────
162+
163+
def test_naive_utc_interpreted_as_utc(self):
164+
"""Naive datetime is treated as UTC (SQLite storage semantics)."""
165+
result = parse_to_utc("2024-01-01T12:00:00")
166+
assert result == datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
167+
168+
# ── Already-aware datetime ──────────────────────────────────────────────
169+
170+
def test_aware_utc_returns_utc_equivalent(self):
171+
"""Already-UTC-aware datetime returns UTC equivalent."""
172+
aware = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
173+
result = parse_to_utc(aware)
174+
assert result == datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
175+
176+
def test_non_utc_aware_converted_to_utc(self):
177+
"""Non-UTC aware datetime is converted to UTC."""
178+
import zoneinfo
179+
est = zoneinfo.ZoneInfo("America/New_York")
180+
aware = datetime(2024, 1, 1, 7, 0, 0, tzinfo=est) # 12:00 UTC
181+
result = parse_to_utc(aware)
182+
assert result == datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
183+
184+
# ── Epoch boundaries ────────────────────────────────────────────────────
185+
186+
def test_epoch_start(self):
187+
"""Unix epoch start is correctly parsed."""
188+
result = parse_to_utc("1970-01-01T00:00:00Z")
189+
assert result == datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)

0 commit comments

Comments
 (0)