File size: 10,584 Bytes
aaa787c
 
 
 
 
 
 
 
 
 
4e6b8c4
 
aaa787c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4e6b8c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
001e8fa
 
4e6b8c4
001e8fa
 
4e6b8c4
 
001e8fa
 
4e6b8c4
001e8fa
4e6b8c4
001e8fa
bce9604
 
 
4e6b8c4
 
 
 
001e8fa
 
 
4e6b8c4
 
 
 
 
 
 
 
001e8fa
4e6b8c4
001e8fa
4e6b8c4
 
 
 
 
 
 
 
 
 
 
 
001e8fa
 
4e6b8c4
001e8fa
4e6b8c4
 
001e8fa
4e6b8c4
 
 
 
 
001e8fa
4e6b8c4
bce9604
4e6b8c4
bce9604
4e6b8c4
 
 
cb59bc2
001e8fa
 
 
cb59bc2
 
 
 
 
847294a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bce9604
 
 
847294a
 
 
bce9604
 
847294a
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
"""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")

        # Timing should still be recorded
        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  # Truncated to 16 chars

    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."""
        # Same session should always produce same hash
        session_id = "user-session-12345"
        hashes = [hash_session_id(session_id) for _ in range(10)]
        assert len(set(hashes)) == 1  # All hashes should be identical


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")

        # Even with valid username, should return default if is_hf_spaces=False
        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