mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-27 15:08:57 -06:00
- Fix keyboard shortcuts (like 'g r' for Go to Reports) incorrectly triggering while typing in input fields, textareas, and rich text editors - Enhance detection for popular rich text editors: * Toast UI Editor (used in project descriptions) * TinyMCE, Quill, CodeMirror, Summernote * All contenteditable elements - Allow specific global shortcuts even in input fields: * Ctrl+K / Cmd+K: Open command palette * Shift+?: Show keyboard shortcuts help * Ctrl+/: Focus search - Clear key sequences when user starts typing to prevent partial matches - Add debug logging for troubleshooting keyboard shortcut issues - Update JavaScript cache busting version numbers (v=2.0, v=2.2) Test improvements: - Add comprehensive test suite for keyboard shortcuts input fix * Test typing 'gr' in 'program' doesn't trigger navigation * Test rich text editor detection logic * Test allowed shortcuts in inputs - Refactor smoke tests to use admin_authenticated_client fixture instead of manual login (DRY principle) - Fix Windows PermissionError in test cleanup for temporary files - Add SESSION_COOKIE_HTTPONLY to test config for security - Update test secret key length to meet requirements - Remove duplicate admin user fixtures Resolves issue where typing words like 'program' or 'graphics' in forms would trigger unintended navigation shortcuts.
277 lines
11 KiB
Python
277 lines
11 KiB
Python
"""
|
|
Tests for telemetry functionality
|
|
"""
|
|
|
|
import pytest
|
|
import os
|
|
import json
|
|
import tempfile
|
|
from unittest.mock import patch, MagicMock
|
|
from app.utils.telemetry import (
|
|
get_telemetry_fingerprint,
|
|
is_telemetry_enabled,
|
|
send_telemetry_ping,
|
|
send_install_ping,
|
|
send_update_ping,
|
|
send_health_ping,
|
|
should_send_telemetry,
|
|
mark_telemetry_sent,
|
|
check_and_send_telemetry
|
|
)
|
|
|
|
|
|
class TestTelemetryFingerprint:
|
|
"""Tests for telemetry fingerprint generation"""
|
|
|
|
def test_fingerprint_is_consistent(self):
|
|
"""Test that fingerprint is consistent for same inputs"""
|
|
with patch.dict(os.environ, {'TELE_SALT': 'test-salt'}):
|
|
fp1 = get_telemetry_fingerprint()
|
|
fp2 = get_telemetry_fingerprint()
|
|
assert fp1 == fp2
|
|
|
|
def test_fingerprint_changes_with_salt(self):
|
|
"""Test that fingerprint changes when salt changes"""
|
|
# Mock the installation config to force fallback to environment variable
|
|
with patch('app.utils.telemetry.get_installation_config') as mock_config:
|
|
mock_config.side_effect = Exception("Force fallback to env var")
|
|
|
|
with patch.dict(os.environ, {'TELE_SALT': 'salt1'}):
|
|
fp1 = get_telemetry_fingerprint()
|
|
|
|
with patch.dict(os.environ, {'TELE_SALT': 'salt2'}):
|
|
fp2 = get_telemetry_fingerprint()
|
|
|
|
assert fp1 != fp2
|
|
|
|
def test_fingerprint_is_sha256_hash(self):
|
|
"""Test that fingerprint is a valid SHA-256 hash"""
|
|
fp = get_telemetry_fingerprint()
|
|
assert len(fp) == 64 # SHA-256 produces 64 hex characters
|
|
assert all(c in '0123456789abcdef' for c in fp)
|
|
|
|
|
|
class TestTelemetryEnabled:
|
|
"""Tests for telemetry enabled check"""
|
|
|
|
@pytest.mark.parametrize('value,expected', [
|
|
('true', True),
|
|
('True', True),
|
|
('TRUE', True),
|
|
('1', True),
|
|
('yes', True),
|
|
('on', True),
|
|
('false', False),
|
|
('False', False),
|
|
('0', False),
|
|
('no', False),
|
|
('', False),
|
|
('random', False),
|
|
])
|
|
def test_telemetry_enabled_values(self, value, expected):
|
|
"""Test various values for ENABLE_TELEMETRY"""
|
|
# Mock installation config to force fallback to environment variable
|
|
with patch('app.utils.telemetry.get_installation_config') as mock_config:
|
|
mock_config.side_effect = Exception("Force fallback to env var")
|
|
with patch.dict(os.environ, {'ENABLE_TELEMETRY': value}):
|
|
assert is_telemetry_enabled() == expected
|
|
|
|
def test_telemetry_disabled_by_default(self):
|
|
"""Test that telemetry is disabled by default"""
|
|
# Mock installation config to force fallback to environment variable
|
|
with patch('app.utils.telemetry.get_installation_config') as mock_config:
|
|
mock_config.side_effect = Exception("Force fallback to env var")
|
|
with patch.dict(os.environ, {}, clear=True):
|
|
assert is_telemetry_enabled() is False
|
|
|
|
|
|
class TestSendTelemetryPing:
|
|
"""Tests for sending telemetry pings"""
|
|
|
|
@patch('app.utils.telemetry.posthog.capture')
|
|
def test_send_ping_when_enabled(self, mock_capture):
|
|
"""Test sending telemetry ping when enabled"""
|
|
with patch.dict(os.environ, {
|
|
'ENABLE_TELEMETRY': 'true',
|
|
'POSTHOG_API_KEY': 'test-api-key',
|
|
'APP_VERSION': '1.0.0',
|
|
'TELE_SALT': 'test-salt'
|
|
}):
|
|
result = send_telemetry_ping('install')
|
|
assert result is True
|
|
assert mock_capture.called
|
|
|
|
# Verify the call
|
|
call_args = mock_capture.call_args
|
|
assert call_args[1]['event'] == 'telemetry.install'
|
|
assert 'distinct_id' in call_args[1]
|
|
assert 'properties' in call_args[1]
|
|
|
|
@patch('app.utils.telemetry.posthog.capture')
|
|
def test_no_ping_when_disabled(self, mock_capture):
|
|
"""Test that no ping is sent when telemetry is disabled"""
|
|
with patch.dict(os.environ, {'ENABLE_TELEMETRY': 'false'}):
|
|
result = send_telemetry_ping('install')
|
|
assert result is False
|
|
assert not mock_capture.called
|
|
|
|
@patch('app.utils.telemetry.posthog.capture')
|
|
def test_no_ping_when_no_api_key(self, mock_capture):
|
|
"""Test that no ping is sent when POSTHOG_API_KEY is not set"""
|
|
with patch.dict(os.environ, {'ENABLE_TELEMETRY': 'true', 'POSTHOG_API_KEY': ''}):
|
|
result = send_telemetry_ping('install')
|
|
assert result is False
|
|
assert not mock_capture.called
|
|
|
|
@patch('app.utils.telemetry.posthog.capture')
|
|
def test_ping_includes_required_fields(self, mock_capture):
|
|
"""Test that telemetry ping includes required fields"""
|
|
with patch.dict(os.environ, {
|
|
'ENABLE_TELEMETRY': 'true',
|
|
'POSTHOG_API_KEY': 'test-api-key',
|
|
'APP_VERSION': '1.0.0',
|
|
'TELE_SALT': 'test-salt'
|
|
}):
|
|
send_telemetry_ping('install', extra_data={'test': 'value'})
|
|
|
|
# Get the call arguments
|
|
call_args = mock_capture.call_args
|
|
event = call_args[1]['event']
|
|
properties = call_args[1]['properties']
|
|
|
|
assert event == 'telemetry.install'
|
|
assert 'app_version' in properties
|
|
assert 'platform' in properties
|
|
assert 'python_version' in properties
|
|
assert 'environment' in properties
|
|
assert 'deployment_method' in properties
|
|
assert properties['test'] == 'value'
|
|
|
|
@patch('app.utils.telemetry.posthog.capture')
|
|
def test_ping_handles_network_errors_gracefully(self, mock_capture):
|
|
"""Test that network errors don't crash the application"""
|
|
mock_capture.side_effect = Exception("Network error")
|
|
|
|
with patch.dict(os.environ, {
|
|
'ENABLE_TELEMETRY': 'true',
|
|
'POSTHOG_API_KEY': 'test-api-key'
|
|
}):
|
|
result = send_telemetry_ping('install')
|
|
assert result is False
|
|
|
|
|
|
class TestTelemetryEventTypes:
|
|
"""Tests for different telemetry event types"""
|
|
|
|
@patch('app.utils.telemetry.send_telemetry_ping')
|
|
def test_send_install_ping(self, mock_send):
|
|
"""Test sending install ping"""
|
|
send_install_ping()
|
|
mock_send.assert_called_once_with(event_type='install')
|
|
|
|
@patch('app.utils.telemetry.send_telemetry_ping')
|
|
def test_send_update_ping(self, mock_send):
|
|
"""Test sending update ping"""
|
|
send_update_ping('1.0.0', '1.1.0')
|
|
mock_send.assert_called_once()
|
|
call_args = mock_send.call_args
|
|
assert call_args[1]['event_type'] == 'update'
|
|
assert call_args[1]['extra_data']['old_version'] == '1.0.0'
|
|
assert call_args[1]['extra_data']['new_version'] == '1.1.0'
|
|
|
|
@patch('app.utils.telemetry.send_telemetry_ping')
|
|
def test_send_health_ping(self, mock_send):
|
|
"""Test sending health ping"""
|
|
send_health_ping()
|
|
mock_send.assert_called_once_with(event_type='health')
|
|
|
|
|
|
class TestTelemetryMarker:
|
|
"""Tests for telemetry marker file functionality"""
|
|
|
|
def test_should_send_when_no_marker(self):
|
|
"""Test that telemetry should be sent when marker doesn't exist"""
|
|
with tempfile.NamedTemporaryFile(delete=True) as tmp:
|
|
marker_path = tmp.name + '_nonexistent'
|
|
with patch.dict(os.environ, {'ENABLE_TELEMETRY': 'true'}):
|
|
assert should_send_telemetry(marker_path) is True
|
|
|
|
def test_should_not_send_when_marker_exists(self):
|
|
"""Test that telemetry shouldn't be sent when marker exists"""
|
|
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
|
marker_path = tmp.name
|
|
try:
|
|
with patch.dict(os.environ, {'ENABLE_TELEMETRY': 'true'}):
|
|
assert should_send_telemetry(marker_path) is False
|
|
finally:
|
|
try:
|
|
os.unlink(marker_path)
|
|
except (PermissionError, OSError):
|
|
pass # Ignore Windows file permission errors
|
|
|
|
def test_mark_telemetry_sent_creates_file(self):
|
|
"""Test that marking telemetry as sent creates marker file"""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
marker_path = os.path.join(tmpdir, 'test_marker')
|
|
with patch.dict(os.environ, {'APP_VERSION': '1.0.0'}):
|
|
mark_telemetry_sent(marker_path)
|
|
assert os.path.exists(marker_path)
|
|
|
|
# Verify file contents
|
|
with open(marker_path, 'r') as f:
|
|
data = json.load(f)
|
|
assert 'version' in data
|
|
assert 'fingerprint' in data
|
|
|
|
|
|
class TestCheckAndSendTelemetry:
|
|
"""Tests for the convenience function"""
|
|
|
|
@patch('app.utils.telemetry.send_install_ping')
|
|
@patch('app.utils.telemetry.mark_telemetry_sent')
|
|
def test_check_and_send_when_appropriate(self, mock_mark, mock_send):
|
|
"""Test that telemetry is sent and marked when appropriate"""
|
|
mock_send.return_value = True
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
marker_path = os.path.join(tmpdir, 'telemetry_sent')
|
|
with patch.dict(os.environ, {
|
|
'ENABLE_TELEMETRY': 'true',
|
|
'TELEMETRY_MARKER_FILE': marker_path
|
|
}):
|
|
result = check_and_send_telemetry()
|
|
assert result is True
|
|
mock_send.assert_called_once()
|
|
mock_mark.assert_called_once()
|
|
|
|
@patch('app.utils.telemetry.send_install_ping')
|
|
def test_no_send_when_disabled(self, mock_send):
|
|
"""Test that telemetry is not sent when disabled"""
|
|
# Mock installation config to force fallback to environment variable
|
|
with patch('app.utils.telemetry.get_installation_config') as mock_config:
|
|
mock_config.side_effect = Exception("Force fallback to env var")
|
|
with patch.dict(os.environ, {'ENABLE_TELEMETRY': 'false'}):
|
|
result = check_and_send_telemetry()
|
|
assert result is False
|
|
assert not mock_send.called
|
|
|
|
@patch('app.utils.telemetry.send_install_ping')
|
|
def test_no_send_when_already_sent(self, mock_send):
|
|
"""Test that telemetry is not sent when already marked as sent"""
|
|
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
|
marker_path = tmp.name
|
|
try:
|
|
with patch.dict(os.environ, {
|
|
'ENABLE_TELEMETRY': 'true',
|
|
'TELEMETRY_MARKER_FILE': marker_path
|
|
}):
|
|
result = check_and_send_telemetry()
|
|
assert result is False
|
|
assert not mock_send.called
|
|
finally:
|
|
try:
|
|
os.unlink(marker_path)
|
|
except (PermissionError, OSError):
|
|
pass # Ignore Windows file permission errors
|
|
|