|
1 | 1 | from datetime import datetime, timezone |
2 | | -from typing import Callable |
| 2 | +from uuid import UUID |
3 | 3 | import pytest |
4 | 4 | import uuid6 |
5 | 5 |
|
@@ -146,96 +146,59 @@ def test_uuid_property() -> None: |
146 | 146 | assert typeid.uuid.time == uuid.time |
147 | 147 |
|
148 | 148 |
|
149 | | -def _extract_ts_ms_from_uuid_bytes(uuid_bytes: bytes) -> int: |
150 | | - """UUIDv7: first 48 bits (6 bytes) are Unix timestamp in milliseconds.""" |
151 | | - assert len(uuid_bytes) == 16 |
152 | | - return int.from_bytes(uuid_bytes[0:6], byteorder="big") |
| 149 | +def test_created_at_none_for_nil_uuid_suffix(): |
| 150 | + tid = TypeID(prefix="x", suffix="00000000000000000000000000") |
| 151 | + assert tid.created_at is None |
| 152 | + |
| 153 | + |
| 154 | +def test_created_at_none_for_non_v7_uuid_v4(): |
| 155 | + # UUIDv4 (random) must not claim created_at |
| 156 | + u = UUID("550e8400-e29b-41d4-a716-446655440000") # version 4 |
| 157 | + tid = TypeID.from_uuid(u, prefix="x") |
| 158 | + assert tid.created_at is None |
| 159 | + |
| 160 | + |
| 161 | +def test_created_at_is_utc_for_uuid7_generated_typeid(): |
| 162 | + # Default TypeID generation should be UUIDv7; then created_at must be present and UTC |
| 163 | + tid = TypeID(prefix="x") |
| 164 | + dt = tid.created_at |
| 165 | + assert dt is not None |
| 166 | + _assert_utc_datetime(dt) |
| 167 | + |
| 168 | + |
| 169 | +def test_created_at_monotonic_increasing_for_multiple_new_ids(): |
| 170 | + # UUIDv7 embeds time; created_at should be non-decreasing across consecutive generations. |
| 171 | + # Note: UUIDv7 can generate multiple IDs within the same millisecond, so equality is allowed. |
| 172 | + t1 = TypeID(prefix="x").created_at |
| 173 | + t2 = TypeID(prefix="x").created_at |
| 174 | + t3 = TypeID(prefix="x").created_at |
| 175 | + |
| 176 | + assert t1 is not None and t2 is not None and t3 is not None |
| 177 | + assert t1 <= t2 <= t3 |
| 178 | + |
| 179 | + |
| 180 | +def test_created_at_does_not_crash_if_uuid_object_is_unexpected(monkeypatch): |
| 181 | + # If TypeID.uuid returns something odd that breaks version/int access, |
| 182 | + # created_at should return None (safe behavior). |
| 183 | + class WeirdUUID: |
| 184 | + @property |
| 185 | + def version(self): |
| 186 | + raise RuntimeError("nope") |
| 187 | + |
| 188 | + @property |
| 189 | + def int(self): |
| 190 | + raise RuntimeError("nope") |
| 191 | + |
| 192 | + tid = TypeID(prefix="x", suffix="00000000000000000000000000") |
| 193 | + |
| 194 | + # monkeypatch instance attribute/property access |
| 195 | + monkeypatch.setattr(type(tid), "uuid", property(lambda self: WeirdUUID())) |
| 196 | + |
| 197 | + assert tid.created_at is None |
153 | 198 |
|
154 | 199 |
|
155 | 200 | def _assert_utc_datetime(dt: datetime) -> None: |
156 | 201 | assert isinstance(dt, datetime) |
157 | | - assert dt.tzinfo is not None |
158 | | - assert dt.tzinfo.utcoffset(dt) == timezone.utc.utcoffset(dt) |
159 | | - |
160 | | - |
161 | | -@pytest.mark.parametrize( |
162 | | - "uuid7_factory", |
163 | | - [ |
164 | | - pytest.param( |
165 | | - lambda: __import__("uuid_utils").uuid7(), |
166 | | - id="uuid-utils", |
167 | | - marks=pytest.mark.skipif( |
168 | | - pytest.importorskip("uuid_utils", reason="uuid-utils not installed") is None, |
169 | | - reason="uuid-utils not installed", |
170 | | - ), |
171 | | - ), |
172 | | - pytest.param( |
173 | | - lambda: __import__("uuid6").uuid7(), |
174 | | - id="uuid6", |
175 | | - marks=pytest.mark.skipif( |
176 | | - pytest.importorskip("uuid6", reason="uuid6 not installed") is None, |
177 | | - reason="uuid6 not installed", |
178 | | - ), |
179 | | - ), |
180 | | - ], |
181 | | -) |
182 | | -def test_timestamp_ms_matches_uuid_bytes_from_uuid(uuid7_factory: Callable[[], object]) -> None: |
183 | | - u = uuid7_factory() |
184 | | - tid = TypeID.from_uuid(prefix="user", suffix=u) |
185 | | - |
186 | | - expected = _extract_ts_ms_from_uuid_bytes(u.bytes) |
187 | | - assert tid.timestamp_ms == expected |
188 | | - |
189 | | - |
190 | | -@pytest.mark.parametrize( |
191 | | - "uuid7_factory", |
192 | | - [ |
193 | | - pytest.param( |
194 | | - lambda: __import__("uuid_utils").uuid7(), |
195 | | - id="uuid-utils", |
196 | | - marks=pytest.mark.skipif( |
197 | | - pytest.importorskip("uuid_utils", reason="uuid-utils not installed") is None, |
198 | | - reason="uuid-utils not installed", |
199 | | - ), |
200 | | - ), |
201 | | - pytest.param( |
202 | | - lambda: __import__("uuid6").uuid7(), |
203 | | - id="uuid6", |
204 | | - marks=pytest.mark.skipif( |
205 | | - pytest.importorskip("uuid6", reason="uuid6 not installed") is None, |
206 | | - reason="uuid6 not installed", |
207 | | - ), |
208 | | - ), |
209 | | - ], |
210 | | -) |
211 | | -def test_creation_time_matches_timestamp_ms_from_uuid(uuid7_factory: Callable[[], object]) -> None: |
212 | | - u = uuid7_factory() |
213 | | - tid = TypeID.from_uuid(prefix="user", suffix=u) |
214 | | - |
215 | | - _assert_utc_datetime(tid.creation_time) |
216 | | - |
217 | | - expected_dt = datetime.fromtimestamp(tid.timestamp_ms / 1000, tz=timezone.utc) |
218 | | - assert tid.creation_time == expected_dt |
219 | | - |
220 | | - |
221 | | -def test_timestamp_ms_roundtrip_from_string() -> None: |
222 | | - # Generate a TypeID (whatever backend is configured) and roundtrip via string parsing. |
223 | | - tid1 = TypeID(prefix="user") |
224 | | - s = str(tid1) |
225 | | - |
226 | | - tid2 = TypeID.from_string(s) |
227 | | - assert tid2.timestamp_ms == tid1.timestamp_ms |
228 | | - assert tid2.creation_time == tid1.creation_time |
229 | | - |
230 | | - |
231 | | -def test_creation_time_is_monotonic_non_decreasing_for_multiple_new() -> None: |
232 | | - # UUIDv7 timestamps are millisecond resolution; two IDs in the same ms may be equal. |
233 | | - t1 = TypeID(prefix="user") |
234 | | - t2 = TypeID(prefix="user") |
235 | | - |
236 | | - assert t2.timestamp_ms >= t1.timestamp_ms |
237 | | - |
238 | | - |
239 | | -def test_creation_time_timezone_is_utc() -> None: |
240 | | - tid = TypeID(prefix="user") |
241 | | - _assert_utc_datetime(tid.creation_time) |
| 202 | + assert dt.tzinfo is timezone.utc |
| 203 | + # must be timezone-aware and normalized to UTC |
| 204 | + assert dt.utcoffset() == timezone.utc.utcoffset(dt) |
0 commit comments