|
|
"""Tests for telemetry utility functions.""" |
|
|
|
|
|
import time |
|
|
|
|
|
import pytest |
|
|
|
|
|
from mosaic.telemetry.utils import ( |
|
|
StageTimer, |
|
|
sanitize_error_message, |
|
|
hash_session_id, |
|
|
UserInfo, |
|
|
extract_user_info, |
|
|
) |
|
|
|
|
|
|
|
|
class TestStageTimer: |
|
|
"""Tests for StageTimer context manager.""" |
|
|
|
|
|
def test_basic_timing(self): |
|
|
"""Test basic timing functionality.""" |
|
|
timings = {} |
|
|
with StageTimer("test_stage", timings): |
|
|
time.sleep(0.1) |
|
|
|
|
|
assert "test_stage_duration_sec" in timings |
|
|
assert timings["test_stage_duration_sec"] >= 0.1 |
|
|
|
|
|
def test_multiple_stages(self): |
|
|
"""Test timing multiple stages.""" |
|
|
timings = {} |
|
|
|
|
|
with StageTimer("stage_a", timings): |
|
|
time.sleep(0.05) |
|
|
|
|
|
with StageTimer("stage_b", timings): |
|
|
time.sleep(0.05) |
|
|
|
|
|
assert "stage_a_duration_sec" in timings |
|
|
assert "stage_b_duration_sec" in timings |
|
|
assert timings["stage_a_duration_sec"] >= 0.05 |
|
|
assert timings["stage_b_duration_sec"] >= 0.05 |
|
|
|
|
|
def test_timing_with_exception(self): |
|
|
"""Test that timing is recorded even when exception occurs.""" |
|
|
timings = {} |
|
|
|
|
|
with pytest.raises(ValueError): |
|
|
with StageTimer("failing_stage", timings): |
|
|
time.sleep(0.05) |
|
|
raise ValueError("Test error") |
|
|
|
|
|
|
|
|
assert "failing_stage_duration_sec" in timings |
|
|
assert timings["failing_stage_duration_sec"] >= 0.05 |
|
|
|
|
|
|
|
|
class TestSanitizeErrorMessage: |
|
|
"""Tests for error message sanitization.""" |
|
|
|
|
|
def test_sanitize_unix_paths(self): |
|
|
"""Test sanitization of Unix-style paths.""" |
|
|
message = "File not found: /home/user/data/slide.svs" |
|
|
sanitized = sanitize_error_message(message) |
|
|
assert "/home/user" not in sanitized |
|
|
assert "[PATH]" in sanitized |
|
|
|
|
|
def test_sanitize_windows_paths(self): |
|
|
"""Test sanitization of Windows-style paths.""" |
|
|
message = "File not found: C:\\Users\\John\\Documents\\slide.svs" |
|
|
sanitized = sanitize_error_message(message) |
|
|
assert "C:\\Users" not in sanitized |
|
|
assert "[PATH]" in sanitized |
|
|
|
|
|
def test_sanitize_ip_addresses(self): |
|
|
"""Test sanitization of IP addresses.""" |
|
|
message = "Connection refused to 192.168.1.100:8080" |
|
|
sanitized = sanitize_error_message(message) |
|
|
assert "192.168.1.100" not in sanitized |
|
|
assert "[IP]" in sanitized |
|
|
|
|
|
def test_sanitize_email_addresses(self): |
|
|
"""Test sanitization of email addresses.""" |
|
|
message = "Invalid user: john.doe@example.com" |
|
|
sanitized = sanitize_error_message(message) |
|
|
assert "john.doe@example.com" not in sanitized |
|
|
assert "[EMAIL]" in sanitized |
|
|
|
|
|
def test_sanitize_urls(self): |
|
|
"""Test sanitization of URLs.""" |
|
|
message = "Failed to fetch https://api.example.com/data" |
|
|
sanitized = sanitize_error_message(message) |
|
|
assert "https://api.example.com" not in sanitized |
|
|
assert "[URL]" in sanitized |
|
|
|
|
|
def test_sanitize_multiple_patterns(self): |
|
|
"""Test sanitization of multiple patterns in one message.""" |
|
|
message = ( |
|
|
"Error at /home/user/app: " |
|
|
"Could not connect to 10.0.0.1 for user@domain.com" |
|
|
) |
|
|
sanitized = sanitize_error_message(message) |
|
|
assert "/home/user" not in sanitized |
|
|
assert "10.0.0.1" not in sanitized |
|
|
assert "user@domain.com" not in sanitized |
|
|
|
|
|
def test_sanitize_empty_message(self): |
|
|
"""Test handling of empty message.""" |
|
|
assert sanitize_error_message("") == "" |
|
|
assert sanitize_error_message(None) is None |
|
|
|
|
|
def test_sanitize_preserves_error_context(self): |
|
|
"""Test that error context is preserved.""" |
|
|
message = "ValueError: Invalid configuration" |
|
|
sanitized = sanitize_error_message(message) |
|
|
assert "ValueError" in sanitized |
|
|
assert "Invalid configuration" in sanitized |
|
|
|
|
|
|
|
|
class TestHashSessionId: |
|
|
"""Tests for session ID hashing.""" |
|
|
|
|
|
def test_hash_session_id(self): |
|
|
"""Test basic session ID hashing.""" |
|
|
hashed = hash_session_id("test-session-123") |
|
|
assert hashed is not None |
|
|
assert hashed != "test-session-123" |
|
|
assert len(hashed) == 16 |
|
|
|
|
|
def test_hash_none_returns_none(self): |
|
|
"""Test that None input returns None.""" |
|
|
assert hash_session_id(None) is None |
|
|
|
|
|
def test_hash_is_deterministic(self): |
|
|
"""Test that same input produces same hash.""" |
|
|
hash1 = hash_session_id("session-abc") |
|
|
hash2 = hash_session_id("session-abc") |
|
|
assert hash1 == hash2 |
|
|
|
|
|
def test_different_inputs_different_hashes(self): |
|
|
"""Test that different inputs produce different hashes.""" |
|
|
hash1 = hash_session_id("session-1") |
|
|
hash2 = hash_session_id("session-2") |
|
|
assert hash1 != hash2 |
|
|
|
|
|
def test_hash_is_consistent_across_calls(self): |
|
|
"""Test hash consistency for privacy linking.""" |
|
|
|
|
|
session_id = "user-session-12345" |
|
|
hashes = [hash_session_id(session_id) for _ in range(10)] |
|
|
assert len(set(hashes)) == 1 |
|
|
|
|
|
|
|
|
class TestUserInfo: |
|
|
"""Tests for UserInfo dataclass.""" |
|
|
|
|
|
def test_default_values(self): |
|
|
"""Test default UserInfo values.""" |
|
|
user_info = UserInfo() |
|
|
assert user_info.is_logged_in is False |
|
|
assert user_info.username is None |
|
|
|
|
|
def test_custom_values(self): |
|
|
"""Test UserInfo with custom values.""" |
|
|
user_info = UserInfo(is_logged_in=True, username="testuser") |
|
|
assert user_info.is_logged_in is True |
|
|
assert user_info.username == "testuser" |
|
|
|
|
|
|
|
|
class TestExtractUserInfo: |
|
|
"""Tests for extract_user_info function.""" |
|
|
|
|
|
def _create_mock_request(self, username: str = None): |
|
|
"""Helper to create a mock Gradio request object. |
|
|
|
|
|
In Gradio 6.x+, the request object has a username attribute. |
|
|
""" |
|
|
|
|
|
class MockRequest: |
|
|
def __init__(self, username): |
|
|
self.username = username |
|
|
|
|
|
return MockRequest(username) |
|
|
|
|
|
def test_extract_user_info_with_logged_in_user(self): |
|
|
"""Test extraction with a logged-in user via OAuthProfile.""" |
|
|
profile = self._create_mock_profile("testuser123") |
|
|
user_info = extract_user_info(None, is_hf_spaces=True, profile=profile) |
|
|
|
|
|
assert user_info.is_logged_in is True |
|
|
assert user_info.username == "testuser123" |
|
|
|
|
|
def test_extract_user_info_anonymous_user(self): |
|
|
"""Test extraction for anonymous user (username=None).""" |
|
|
request = self._create_mock_request(None) |
|
|
|
|
|
user_info = extract_user_info(request, is_hf_spaces=True) |
|
|
|
|
|
assert user_info.is_logged_in is False |
|
|
assert user_info.username is None |
|
|
|
|
|
def test_extract_user_info_not_hf_spaces(self): |
|
|
"""Test extraction when not on HF Spaces.""" |
|
|
request = self._create_mock_request("testuser") |
|
|
|
|
|
|
|
|
user_info = extract_user_info(request, is_hf_spaces=False) |
|
|
|
|
|
assert user_info.is_logged_in is False |
|
|
assert user_info.username is None |
|
|
|
|
|
def test_extract_user_info_none_request(self): |
|
|
"""Test extraction with None request.""" |
|
|
user_info = extract_user_info(None, is_hf_spaces=True) |
|
|
|
|
|
assert user_info.is_logged_in is False |
|
|
assert user_info.username is None |
|
|
|
|
|
def test_extract_user_info_request_without_username_attr(self): |
|
|
"""Test extraction when request doesn't have username attribute.""" |
|
|
|
|
|
class RequestWithoutUsername: |
|
|
pass |
|
|
|
|
|
request = RequestWithoutUsername() |
|
|
user_info = extract_user_info(request, is_hf_spaces=True) |
|
|
|
|
|
assert user_info.is_logged_in is False |
|
|
assert user_info.username is None |
|
|
|
|
|
def test_extract_user_info_with_special_characters(self): |
|
|
"""Test extraction with username containing special characters.""" |
|
|
profile = self._create_mock_profile("user-name_123") |
|
|
|
|
|
user_info = extract_user_info(None, is_hf_spaces=True, profile=profile) |
|
|
|
|
|
assert user_info.is_logged_in is True |
|
|
assert user_info.username == "user-name_123" |
|
|
|
|
|
def test_extract_user_info_empty_string_username(self): |
|
|
"""Test extraction with empty string username (treated as not logged in).""" |
|
|
request = self._create_mock_request("") |
|
|
|
|
|
user_info = extract_user_info(request, is_hf_spaces=True) |
|
|
|
|
|
assert user_info.is_logged_in is False |
|
|
assert user_info.username is None |
|
|
|
|
|
def _create_mock_profile(self, username: str = None): |
|
|
"""Helper to create a mock OAuthProfile object.""" |
|
|
|
|
|
class MockOAuthProfile: |
|
|
def __init__(self, username): |
|
|
self.username = username |
|
|
|
|
|
if username is None: |
|
|
return None |
|
|
return MockOAuthProfile(username) |
|
|
|
|
|
def test_extract_user_info_from_oauth_profile(self): |
|
|
"""Test extraction from OAuthProfile (primary path for LoginButton).""" |
|
|
profile = self._create_mock_profile("oauth_user") |
|
|
user_info = extract_user_info(None, is_hf_spaces=True, profile=profile) |
|
|
|
|
|
assert user_info.is_logged_in is True |
|
|
assert user_info.username == "oauth_user" |
|
|
|
|
|
def test_extract_user_info_oauth_profile_takes_precedence(self): |
|
|
"""Test that OAuthProfile takes precedence over request.username.""" |
|
|
request = self._create_mock_request("request_user") |
|
|
profile = self._create_mock_profile("oauth_user") |
|
|
|
|
|
user_info = extract_user_info(request, is_hf_spaces=True, profile=profile) |
|
|
|
|
|
assert user_info.is_logged_in is True |
|
|
assert user_info.username == "oauth_user" |
|
|
|
|
|
def test_extract_user_info_ignores_request_username(self): |
|
|
"""Test that request.username is NOT used (it returns Space owner, not visitor).""" |
|
|
request = self._create_mock_request("space_owner") |
|
|
|
|
|
user_info = extract_user_info(request, is_hf_spaces=True, profile=None) |
|
|
|
|
|
assert user_info.is_logged_in is False |
|
|
assert user_info.username is None |
|
|
|
|
|
def test_extract_user_info_no_profile_not_hf_spaces(self): |
|
|
"""Test that profile is ignored when not on HF Spaces.""" |
|
|
profile = self._create_mock_profile("oauth_user") |
|
|
|
|
|
user_info = extract_user_info(None, is_hf_spaces=False, profile=profile) |
|
|
|
|
|
assert user_info.is_logged_in is False |
|
|
assert user_info.username is None |
|
|
|