mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
663 lines
25 KiB
Python
663 lines
25 KiB
Python
"""
|
|
Tests for TimeSync Tool
|
|
|
|
Tests the NTP-based time synchronization and FastDate64 timestamp encoding functionality.
|
|
These tests mock network operations to avoid real UDP calls and ensure reliable testing.
|
|
"""
|
|
|
|
import time
|
|
import threading
|
|
from unittest.mock import patch, MagicMock
|
|
import pytest
|
|
from datetime import datetime, timezone
|
|
|
|
from nodupe.tools.time_sync import TimeSyncTool
|
|
|
|
|
|
class TestTimeSyncTool:
|
|
"""Test suite for TimeSyncTool."""
|
|
|
|
def test_tool_metadata(self):
|
|
"""Test that tool metadata is correctly defined."""
|
|
tool = TimeSyncTool()
|
|
metadata = tool.metadata
|
|
|
|
assert metadata.name == "TimeSync"
|
|
assert metadata.version == "1.0.0"
|
|
assert "NTP-based" in metadata.description
|
|
assert "FastDate64" in metadata.description
|
|
assert "time" in metadata.tags
|
|
assert "ntp" in metadata.tags
|
|
|
|
def test_initialization_defaults(self):
|
|
"""Test tool initialization with default values."""
|
|
tool = TimeSyncTool()
|
|
|
|
assert tool.servers == ["time.google.com", "time.cloudflare.com", "pool.ntp.org"]
|
|
assert tool.timeout == 3.0
|
|
assert tool.attempts == 2
|
|
assert tool.max_acceptable_delay == 0.5
|
|
assert tool.alpha == 0.3
|
|
|
|
def test_initialization_custom_values(self):
|
|
"""Test tool initialization with custom values."""
|
|
tool = TimeSyncTool(
|
|
servers=["custom.ntp.com"],
|
|
timeout=5.0,
|
|
attempts=3,
|
|
max_acceptable_delay=1.0,
|
|
smoothing_alpha=0.5
|
|
)
|
|
|
|
assert tool.servers == ["custom.ntp.com"]
|
|
assert tool.timeout == 5.0
|
|
assert tool.attempts == 3
|
|
assert tool.max_acceptable_delay == 1.0
|
|
assert tool.alpha == 0.5
|
|
|
|
def test_runtime_flags_initial_state(self):
|
|
"""Test initial state of runtime flags."""
|
|
tool = TimeSyncTool()
|
|
|
|
# These depend on environment variables, so we just check they're booleans
|
|
assert isinstance(tool.is_enabled(), bool)
|
|
assert isinstance(tool.is_network_allowed(), bool)
|
|
assert isinstance(tool.is_background_allowed(), bool)
|
|
|
|
def test_enable_disable_tool(self):
|
|
"""Test enabling and disabling the tool."""
|
|
tool = TimeSyncTool(enabled=False)
|
|
|
|
assert not tool.is_enabled()
|
|
|
|
tool.enable()
|
|
assert tool.is_enabled()
|
|
|
|
tool.disable()
|
|
assert not tool.is_enabled()
|
|
|
|
def test_network_operations_control(self):
|
|
"""Test enabling and disabling network operations."""
|
|
tool = TimeSyncTool(allow_network=False)
|
|
|
|
assert not tool.is_network_allowed()
|
|
|
|
tool.enable_network()
|
|
assert tool.is_network_allowed()
|
|
|
|
tool.disable_network()
|
|
assert not tool.is_network_allowed()
|
|
|
|
def test_background_sync_control(self):
|
|
"""Test enabling and disabling background synchronization."""
|
|
tool = TimeSyncTool(allow_background=False)
|
|
|
|
assert not tool.is_background_allowed()
|
|
|
|
tool.enable_background()
|
|
assert tool.is_background_allowed()
|
|
|
|
tool.disable_background()
|
|
assert not tool.is_background_allowed()
|
|
|
|
def test_encode_decode_fastdate64_roundtrip(self):
|
|
"""Test FastDate64 encoding and decoding roundtrip."""
|
|
# Test with various timestamps
|
|
test_timestamps = [
|
|
1672531200.123456, # 2023-01-01T00:00:00.123456Z
|
|
0.0, # Unix epoch
|
|
1000000000.0, # 2001-09-09T01:46:40Z
|
|
time.time(), # Current time
|
|
]
|
|
|
|
for ts in test_timestamps:
|
|
encoded = TimeSyncTool.encode_fastdate64(ts)
|
|
decoded = TimeSyncTool.decode_fastdate64(encoded)
|
|
|
|
# Allow small rounding error due to fractional truncation
|
|
assert abs(decoded - ts) < 1e-6, f"Roundtrip failed for {ts}"
|
|
|
|
def test_encode_fastdate64_negative_timestamp(self):
|
|
"""Test that negative timestamps raise ValueError."""
|
|
with pytest.raises(ValueError, match="Negative timestamps not supported"):
|
|
TimeSyncTool.encode_fastdate64(-1.0)
|
|
|
|
def test_encode_fastdate64_overflow(self):
|
|
"""Test that timestamps too large for encoding raise OverflowError."""
|
|
# Use a timestamp that would exceed FASTDATE_SECONDS_BITS
|
|
large_ts = (1 << 34) # Exceeds 34-bit seconds field
|
|
|
|
with pytest.raises(OverflowError, match="too large for.*bit field"):
|
|
TimeSyncTool.encode_fastdate64(large_ts)
|
|
|
|
def test_fastdate64_to_iso_conversion(self):
|
|
"""Test FastDate64 to ISO 8601 string conversion."""
|
|
ts = 1672531200.123456 # 2023-01-01T00:00:00.123456Z
|
|
encoded = TimeSyncTool.encode_fastdate64(ts)
|
|
iso_string = TimeSyncTool.fastdate64_to_iso(encoded)
|
|
|
|
# Parse the ISO string back to timestamp
|
|
dt = datetime.fromisoformat(iso_string)
|
|
if dt.tzinfo is None:
|
|
dt = dt.replace(tzinfo=timezone.utc)
|
|
decoded_ts = dt.timestamp()
|
|
|
|
assert abs(decoded_ts - ts) < 1e-6
|
|
|
|
def test_iso_to_fastdate64_conversion(self):
|
|
"""Test ISO 8601 string to FastDate64 conversion."""
|
|
iso_string = "2023-01-01T00:00:00.123456+00:00"
|
|
encoded = TimeSyncTool.iso_to_fastdate64(iso_string)
|
|
decoded = TimeSyncTool.decode_fastdate64(encoded)
|
|
|
|
# Parse the original ISO string to timestamp
|
|
dt = datetime.fromisoformat(iso_string)
|
|
if dt.tzinfo is None:
|
|
dt = dt.replace(tzinfo=timezone.utc)
|
|
original_ts = dt.timestamp()
|
|
|
|
assert abs(decoded - original_ts) < 1e-6
|
|
|
|
def test_disabled_behavior_fallback(self):
|
|
"""Test that disabled tool falls back to time.monotonic()."""
|
|
tool = TimeSyncTool(enabled=False)
|
|
|
|
# Should not raise and should return a float
|
|
result = tool.get_corrected_time()
|
|
assert isinstance(result, float)
|
|
|
|
# Should return a valid FastDate64 encoded timestamp
|
|
fast64 = tool.get_corrected_fast64()
|
|
assert isinstance(fast64, int)
|
|
|
|
# Should raise when trying to sync
|
|
with pytest.raises(TimeSyncTool._get_exception_class()):
|
|
tool.force_sync()
|
|
|
|
@patch('nodupe.tools.time_sync.socket.getaddrinfo')
|
|
@patch('nodupe.tools.time_sync.TimeSyncTool._query_address')
|
|
def test_force_sync_success(self, mock_query_address, mock_getaddrinfo):
|
|
"""Test successful NTP synchronization."""
|
|
# Mock DNS resolution
|
|
mock_getaddrinfo.return_value = [
|
|
(socket.AF_INET, socket.SOCK_DGRAM, 17, '', ('1.1.1.1', 123))
|
|
]
|
|
|
|
# Mock NTP response
|
|
server_time = 1600000000.5
|
|
offset = 0.25
|
|
delay = 0.01
|
|
mock_query_address.return_value = (server_time, offset, delay)
|
|
|
|
tool = TimeSyncTool(servers=["test.ntp.com"], enabled=True, allow_network=True)
|
|
|
|
result = tool.force_sync()
|
|
|
|
assert result[0] == "test.ntp.com"
|
|
assert result[1] == server_time
|
|
assert result[2] == offset
|
|
assert result[3] == delay
|
|
|
|
# Verify that internal state was updated
|
|
assert tool.get_offset_estimate() is not None
|
|
assert tool.get_last_delay() == delay
|
|
|
|
@patch('nodupe.tools.time_sync.socket.getaddrinfo')
|
|
def test_force_sync_disabled_network(self, mock_getaddrinfo):
|
|
"""Test that force_sync raises when network is disabled."""
|
|
tool = TimeSyncTool(allow_network=False)
|
|
|
|
with pytest.raises(TimeSyncTool._get_exception_class(), match="Network operations are disabled"):
|
|
tool.force_sync()
|
|
|
|
@patch('nodupe.tools.time_sync.socket.getaddrinfo')
|
|
def test_force_sync_disabled_tool(self, mock_getaddrinfo):
|
|
"""Test that force_sync raises when tool is disabled."""
|
|
tool = TimeSyncTool(enabled=False)
|
|
|
|
with pytest.raises(TimeSyncTool._get_exception_class(), match="TimeSync instance is disabled"):
|
|
tool.force_sync()
|
|
|
|
@patch('nodupe.tools.time_sync.socket.getaddrinfo')
|
|
@patch('nodupe.tools.time_sync.TimeSyncTool._query_address')
|
|
def test_force_sync_high_delay(self, mock_query_address, mock_getaddrinfo):
|
|
"""Test that force_sync raises when delay exceeds threshold."""
|
|
# Mock DNS resolution
|
|
mock_getaddrinfo.return_value = [
|
|
(socket.AF_INET, socket.SOCK_DGRAM, 17, '', ('1.1.1.1', 123))
|
|
]
|
|
|
|
# Mock NTP response with high delay
|
|
server_time = 1600000000.5
|
|
offset = 0.25
|
|
delay = 1.0 # Exceeds max_acceptable_delay of 0.5
|
|
mock_query_address.return_value = (server_time, offset, delay)
|
|
|
|
tool = TimeSyncTool(servers=["test.ntp.com"], enabled=True, allow_network=True)
|
|
|
|
with pytest.raises(RuntimeError, match="too noisy"):
|
|
tool.force_sync()
|
|
|
|
@patch('nodupe.tools.time_sync.socket.getaddrinfo')
|
|
@patch('nodupe.tools.time_sync.TimeSyncTool._query_address')
|
|
def test_maybe_sync_success(self, mock_query_address, mock_getaddrinfo):
|
|
"""Test successful maybe_sync operation."""
|
|
# Mock DNS resolution
|
|
mock_getaddrinfo.return_value = [
|
|
(socket.AF_INET, socket.SOCK_DGRAM, 17, '', ('1.1.1.1', 123))
|
|
]
|
|
|
|
# Mock NTP response
|
|
server_time = 1600000000.5
|
|
offset = 0.25
|
|
delay = 0.01
|
|
mock_query_address.return_value = (server_time, offset, delay)
|
|
|
|
tool = TimeSyncTool(servers=["test.ntp.com"], enabled=True, allow_network=True)
|
|
|
|
result = tool.maybe_sync()
|
|
|
|
assert result is not None
|
|
assert result[1] == server_time
|
|
|
|
def test_maybe_sync_disabled(self):
|
|
"""Test maybe_sync returns None when disabled."""
|
|
tool = TimeSyncTool(enabled=False)
|
|
|
|
result = tool.maybe_sync()
|
|
assert result is None
|
|
|
|
@patch('nodupe.tools.time_sync.socket.getaddrinfo')
|
|
@patch('nodupe.tools.time_sync.TimeSyncTool._query_address')
|
|
def test_maybe_sync_failure(self, mock_query_address, mock_getaddrinfo):
|
|
"""Test maybe_sync returns None on failure."""
|
|
# Mock DNS resolution failure
|
|
mock_getaddrinfo.return_value = []
|
|
|
|
tool = TimeSyncTool(servers=["test.ntp.com"], enabled=True, allow_network=True)
|
|
|
|
result = tool.maybe_sync()
|
|
assert result is None
|
|
|
|
@patch('nodupe.tools.time_sync.socket.getaddrinfo')
|
|
@patch('nodupe.tools.time_sync.TimeSyncTool._query_address')
|
|
def test_background_sync_start_stop(self, mock_query_address, mock_getaddrinfo):
|
|
"""Test starting and stopping background synchronization."""
|
|
# Mock DNS resolution and NTP response
|
|
mock_getaddrinfo.return_value = [
|
|
(socket.AF_INET, socket.SOCK_DGRAM, 17, '', ('1.1.1.1', 123))
|
|
]
|
|
mock_query_address.return_value = (1600000000.5, 0.25, 0.01)
|
|
|
|
tool = TimeSyncTool(
|
|
servers=["test.ntp.com"],
|
|
enabled=True,
|
|
allow_network=True,
|
|
allow_background=True
|
|
)
|
|
|
|
# Start background sync
|
|
tool.start_background(interval=1.0, initial_sync=False)
|
|
|
|
# Give thread time to start
|
|
time.sleep(0.1)
|
|
|
|
assert tool._bg_thread is not None
|
|
assert tool._bg_thread.is_alive()
|
|
|
|
# Stop background sync
|
|
tool.stop_background(wait=True)
|
|
|
|
assert tool._bg_thread is None
|
|
|
|
def test_background_sync_disabled_network(self):
|
|
"""Test that background sync cannot start when network is disabled."""
|
|
tool = TimeSyncTool(allow_network=False, allow_background=True)
|
|
|
|
with pytest.raises(TimeSyncTool._get_exception_class(), match="Cannot start background sync"):
|
|
tool.start_background()
|
|
|
|
def test_background_sync_disabled_background(self):
|
|
"""Test that background sync cannot start when background is disabled."""
|
|
tool = TimeSyncTool(allow_network=True, allow_background=False)
|
|
|
|
with pytest.raises(TimeSyncTool._get_exception_class(), match="Background syncing is disabled"):
|
|
tool.start_background()
|
|
|
|
def test_get_corrected_time_no_sync(self):
|
|
"""Test get_corrected_time returns fallback when not synced."""
|
|
tool = TimeSyncTool(enabled=True)
|
|
|
|
# Should return time.monotonic() when not synced
|
|
result = tool.get_corrected_time()
|
|
assert isinstance(result, float)
|
|
|
|
@patch('nodupe.tools.time_sync.socket.getaddrinfo')
|
|
@patch('nodupe.tools.time_sync.TimeSyncTool._query_address')
|
|
def test_get_corrected_time_after_sync(self, mock_query_address, mock_getaddrinfo):
|
|
"""Test get_corrected_time returns corrected time after sync."""
|
|
# Mock DNS resolution
|
|
mock_getaddrinfo.return_value = [
|
|
(socket.AF_INET, socket.SOCK_DGRAM, 17, '', ('1.1.1.1', 123))
|
|
]
|
|
|
|
# Mock NTP response
|
|
server_time = 1600000000.5
|
|
offset = 0.25
|
|
delay = 0.01
|
|
mock_query_address.return_value = (server_time, offset, delay)
|
|
|
|
tool = TimeSyncTool(servers=["test.ntp.com"], enabled=True, allow_network=True)
|
|
tool.force_sync()
|
|
|
|
# Get corrected time
|
|
corrected_time = tool.get_corrected_time()
|
|
|
|
# Should be close to server time plus elapsed monotonic time
|
|
assert isinstance(corrected_time, float)
|
|
assert corrected_time > server_time
|
|
|
|
def test_get_status(self):
|
|
"""Test get_status returns comprehensive status information."""
|
|
tool = TimeSyncTool(
|
|
servers=["test.ntp.com"],
|
|
timeout=5.0,
|
|
attempts=3,
|
|
max_acceptable_delay=1.0,
|
|
smoothing_alpha=0.5,
|
|
enabled=True,
|
|
allow_network=True,
|
|
allow_background=True
|
|
)
|
|
|
|
status = tool.get_status()
|
|
|
|
expected_keys = [
|
|
"enabled", "network_allowed", "background_allowed", "background_running",
|
|
"base_server_time", "base_monotonic", "smoothed_offset", "last_delay",
|
|
"servers", "timeout", "attempts", "max_acceptable_delay", "smoothing_alpha"
|
|
]
|
|
|
|
for key in expected_keys:
|
|
assert key in status
|
|
|
|
assert status["servers"] == ["test.ntp.com"]
|
|
assert status["timeout"] == 5.0
|
|
assert status["attempts"] == 3
|
|
assert status["max_acceptable_delay"] == 1.0
|
|
assert status["smoothing_alpha"] == 0.5
|
|
|
|
def test_convenience_methods(self):
|
|
"""Test convenience methods work correctly."""
|
|
tool = TimeSyncTool(enabled=False)
|
|
|
|
# These should work without raising
|
|
timestamp = tool.get_timestamp()
|
|
fast64 = tool.get_timestamp_fast64()
|
|
|
|
assert isinstance(timestamp, float)
|
|
assert isinstance(fast64, int)
|
|
|
|
def test_smoothing_alpha_applied(self):
|
|
"""Test that smoothing alpha is applied to offset calculations."""
|
|
tool = TimeSyncTool(smoothing_alpha=0.1, enabled=True, allow_network=True)
|
|
|
|
# Mock multiple syncs with different offsets
|
|
with patch('nodupe.tools.time_sync.socket.getaddrinfo') as mock_getaddrinfo, \
|
|
patch('nodupe.tools.time_sync.TimeSyncTool._query_address') as mock_query_address:
|
|
|
|
mock_getaddrinfo.return_value = [
|
|
(socket.AF_INET, socket.SOCK_DGRAM, 17, '', ('1.1.1.1', 123))
|
|
]
|
|
|
|
# First sync with offset 1.0
|
|
mock_query_address.return_value = (1600000000.0, 1.0, 0.01)
|
|
tool.force_sync()
|
|
first_offset = tool.get_offset_estimate()
|
|
|
|
# Second sync with offset 2.0
|
|
mock_query_address.return_value = (1600000000.0, 2.0, 0.01)
|
|
tool.force_sync()
|
|
second_offset = tool.get_offset_estimate()
|
|
|
|
# Should be smoothed: 0.1 * 2.0 + 0.9 * 1.0 = 1.1
|
|
expected = 0.1 * 2.0 + 0.9 * 1.0
|
|
assert abs(second_offset - expected) < 1e-6
|
|
|
|
def test_tool_lifecycle(self):
|
|
"""Test tool initialization and shutdown lifecycle."""
|
|
tool = TimeSyncTool(enabled=True)
|
|
|
|
# Mock successful sync during initialization
|
|
with patch.object(tool, 'force_sync') as mock_sync:
|
|
tool.initialize()
|
|
mock_sync.assert_called_once()
|
|
|
|
# Shutdown should stop background thread
|
|
with patch.object(tool, 'stop_background') as mock_stop:
|
|
tool.shutdown()
|
|
mock_stop.assert_called_once_with(wait=False)
|
|
|
|
def test_get_authenticated_time_iso8601_format(self):
|
|
"""Test get_authenticated_time with ISO-8601 format."""
|
|
tool = TimeSyncTool(enabled=True)
|
|
|
|
# Mock sync_with_fallback to return a known time
|
|
with patch.object(tool, 'sync_with_fallback') as mock_sync, \
|
|
patch.object(tool, 'get_corrected_time') as mock_get_time:
|
|
|
|
mock_sync.return_value = ("ntp", 1600000000.0, 0.0, 0.01)
|
|
mock_get_time.return_value = 1600000000.123456
|
|
|
|
result = tool.get_authenticated_time()
|
|
|
|
# Should return ISO-8601 format
|
|
assert result == "2020-09-13T12:26:40.123456Z"
|
|
assert isinstance(result, str)
|
|
|
|
def test_get_authenticated_time_unix_format(self):
|
|
"""Test get_authenticated_time with Unix timestamp format."""
|
|
tool = TimeSyncTool(enabled=True)
|
|
|
|
# Mock sync_with_fallback to return a known time
|
|
with patch.object(tool, 'sync_with_fallback') as mock_sync, \
|
|
patch.object(tool, 'get_corrected_time') as mock_get_time:
|
|
|
|
mock_sync.return_value = ("ntp", 1600000000.0, 0.0, 0.01)
|
|
mock_get_time.return_value = 1600000000.123456
|
|
|
|
result = tool.get_authenticated_time(format="unix")
|
|
|
|
# Should return Unix timestamp as string
|
|
assert result == "1600000000.123456"
|
|
assert isinstance(result, str)
|
|
|
|
def test_get_authenticated_time_rfc3339_format(self):
|
|
"""Test get_authenticated_time with RFC-3339 format."""
|
|
tool = TimeSyncTool(enabled=True)
|
|
|
|
# Mock sync_with_fallback to return a known time
|
|
with patch.object(tool, 'sync_with_fallback') as mock_sync, \
|
|
patch.object(tool, 'get_corrected_time') as mock_get_time:
|
|
|
|
mock_sync.return_value = ("ntp", 1600000000.0, 0.0, 0.01)
|
|
mock_get_time.return_value = 1600000000.123456
|
|
|
|
result = tool.get_authenticated_time(format="rfc3339")
|
|
|
|
# Should return RFC-3339 format (same as ISO-8601)
|
|
assert result == "2020-09-13T12:26:40.123456Z"
|
|
|
|
def test_get_authenticated_time_human_format(self):
|
|
"""Test get_authenticated_time with human-readable format."""
|
|
tool = TimeSyncTool(enabled=True)
|
|
|
|
# Mock sync_with_fallback to return a known time
|
|
with patch.object(tool, 'sync_with_fallback') as mock_sync, \
|
|
patch.object(tool, 'get_corrected_time') as mock_get_time:
|
|
|
|
mock_sync.return_value = ("ntp", 1600000000.0, 0.0, 0.01)
|
|
mock_get_time.return_value = 1600000000.123456
|
|
|
|
result = tool.get_authenticated_time(format="human")
|
|
|
|
# Should return human-readable format
|
|
assert result == "2020-09-13 12:26:40.123456 UTC"
|
|
|
|
def test_get_authenticated_time_disabled_tool(self):
|
|
"""Test get_authenticated_time raises error when tool is disabled."""
|
|
tool = TimeSyncTool(enabled=False)
|
|
|
|
with pytest.raises(TimeSyncTool._get_exception_class(), match="TimeSync instance is disabled"):
|
|
tool.get_authenticated_time()
|
|
|
|
def test_get_authenticated_time_unsupported_format(self):
|
|
"""Test get_authenticated_time raises error for unsupported formats."""
|
|
tool = TimeSyncTool(enabled=True)
|
|
|
|
with pytest.raises(ValueError, match="Unsupported format"):
|
|
tool.get_authenticated_time(format="invalid")
|
|
|
|
def test_get_authenticated_time_fallback_warning(self):
|
|
"""Test that fallback to RTC triggers appropriate warning."""
|
|
tool = TimeSyncTool(enabled=True, allow_network=False)
|
|
|
|
# Mock sync_with_fallback to return RTC fallback
|
|
with patch.object(tool, 'sync_with_fallback') as mock_sync, \
|
|
patch.object(tool, 'get_corrected_time') as mock_get_time, \
|
|
patch('nodupe.tools.time_sync.logger') as mock_logger:
|
|
|
|
mock_sync.return_value = ("rtc", 1600000000.0, 0.0, 0.0)
|
|
mock_get_time.return_value = 1600000000.123456
|
|
|
|
result = tool.get_authenticated_time()
|
|
|
|
# Should return the time
|
|
assert result == "2020-09-13T12:26:40.123456Z"
|
|
|
|
# Should log warning about fallback
|
|
mock_logger.warning.assert_called_once_with(
|
|
"Time obtained via rtc fallback (not NTP/NTS). Time may have slight drift from network time."
|
|
)
|
|
|
|
def test_get_authenticated_time_monotonic_fallback(self):
|
|
"""Test get_authenticated_time with monotonic fallback."""
|
|
tool = TimeSyncTool(enabled=True, allow_network=False)
|
|
|
|
# Mock sync_with_fallback to return monotonic fallback
|
|
with patch.object(tool, 'sync_with_fallback') as mock_sync, \
|
|
patch.object(tool, 'get_corrected_time') as mock_get_time, \
|
|
patch('nodupe.tools.time_sync.logger') as mock_logger:
|
|
|
|
mock_sync.return_value = ("monotonic", 1600000000.0, 0.0, 0.0)
|
|
mock_get_time.return_value = 1600000000.123456
|
|
|
|
result = tool.get_authenticated_time()
|
|
|
|
# Should return the time
|
|
assert result == "2020-09-13T12:26:40.123456Z"
|
|
|
|
# Should log warning about fallback
|
|
mock_logger.warning.assert_called_once_with(
|
|
"Time obtained via monotonic fallback (not NTP/NTS). Time may have slight drift from network time."
|
|
)
|
|
|
|
def test_get_authenticated_time_precision(self):
|
|
"""Test that get_authenticated_time maintains microsecond precision."""
|
|
tool = TimeSyncTool(enabled=True)
|
|
|
|
# Test with various microsecond values
|
|
test_times = [
|
|
1600000000.000000, # No fractional part
|
|
1600000000.123456, # 6-digit fractional
|
|
1600000000.100000, # 1-digit fractional
|
|
1600000000.000001, # 1-microsecond
|
|
]
|
|
|
|
for test_time in test_times:
|
|
with patch.object(tool, 'sync_with_fallback') as mock_sync, \
|
|
patch.object(tool, 'get_corrected_time') as mock_get_time:
|
|
|
|
mock_sync.return_value = ("ntp", test_time, 0.0, 0.01)
|
|
mock_get_time.return_value = test_time
|
|
|
|
result = tool.get_authenticated_time()
|
|
|
|
# Parse the ISO string back to timestamp
|
|
dt = datetime.fromisoformat(result.replace("Z", "+00:00"))
|
|
decoded_time = dt.timestamp()
|
|
|
|
# Should maintain microsecond precision
|
|
assert abs(decoded_time - test_time) < 1e-6
|
|
|
|
def test_get_authenticated_time_utc_timezone(self):
|
|
"""Test that get_authenticated_time always returns UTC time."""
|
|
tool = TimeSyncTool(enabled=True)
|
|
|
|
# Mock sync_with_fallback to return a known time
|
|
with patch.object(tool, 'sync_with_fallback') as mock_sync, \
|
|
patch.object(tool, 'get_corrected_time') as mock_get_time:
|
|
|
|
mock_sync.return_value = ("ntp", 1600000000.0, 0.0, 0.01)
|
|
mock_get_time.return_value = 1600000000.123456
|
|
|
|
result = tool.get_authenticated_time()
|
|
|
|
# Should end with Z (UTC indicator)
|
|
assert result.endswith("Z")
|
|
|
|
# Parse and verify timezone is UTC
|
|
dt = datetime.fromisoformat(result.replace("Z", "+00:00"))
|
|
assert dt.tzinfo is not None
|
|
assert dt.tzinfo.utcoffset(dt).total_seconds() == 0
|
|
|
|
def test_get_authenticated_time_network_failure_fallback(self):
|
|
"""Test get_authenticated_time handles network failures gracefully."""
|
|
tool = TimeSyncTool(enabled=True, allow_network=True)
|
|
|
|
# Mock sync_with_fallback to simulate network failure and RTC fallback
|
|
with patch.object(tool, 'sync_with_fallback') as mock_sync, \
|
|
patch.object(tool, 'get_corrected_time') as mock_get_time, \
|
|
patch('nodupe.tools.time_sync.logger') as mock_logger:
|
|
|
|
# Simulate network failure, fallback to RTC
|
|
mock_sync.return_value = ("rtc", 1600000000.0, 0.0, 0.0)
|
|
mock_get_time.return_value = 1600000000.123456
|
|
|
|
result = tool.get_authenticated_time()
|
|
|
|
# Should still return a valid time
|
|
assert result == "2020-09-13T12:26:40.123456Z"
|
|
|
|
# Should log warning about fallback
|
|
mock_logger.warning.assert_called_once()
|
|
|
|
def test_get_authenticated_time_case_insensitive_formats(self):
|
|
"""Test that get_authenticated_time handles case-insensitive format strings."""
|
|
tool = TimeSyncTool(enabled=True)
|
|
|
|
# Mock sync_with_fallback to return a known time
|
|
with patch.object(tool, 'sync_with_fallback') as mock_sync, \
|
|
patch.object(tool, 'get_corrected_time') as mock_get_time:
|
|
|
|
mock_sync.return_value = ("ntp", 1600000000.0, 0.0, 0.01)
|
|
mock_get_time.return_value = 1600000000.123456
|
|
|
|
# Test various case combinations
|
|
test_formats = ["ISO8601", "Iso8601", "ISO", "iso", "RFC3339", "Rfc3339", "rfc", "UNIX", "Unix", "unix", "HUMAN", "Human", "human"]
|
|
|
|
for fmt in test_formats:
|
|
result = tool.get_authenticated_time(format=fmt)
|
|
|
|
if fmt.lower() in ["iso8601", "iso", "rfc3339", "rfc"]:
|
|
assert result == "2020-09-13T12:26:40.123456Z"
|
|
elif fmt.lower() in ["unix", "unix"]:
|
|
assert result == "1600000000.123456"
|
|
elif fmt.lower() in ["human", "human"]:
|
|
assert result == "2020-09-13 12:26:40.123456 UTC"
|
|
|
|
|
|
# Helper function to get the actual exception class
|
|
def _get_exception_class():
|
|
"""Helper to get the actual TimeSyncDisabledError class."""
|
|
return TimeSyncTool._get_exception_class()
|