Files
trailofbits-dropkit/tests/test_version_check.py
T
2026-01-29 21:53:58 -05:00

314 lines
10 KiB
Python

"""Tests for version_check module."""
import json
import subprocess
import time
from unittest.mock import Mock, patch
from dropkit.version_check import (
commits_differ,
extract_commit_from_version,
get_last_check_file,
get_latest_git_commit,
should_check_version,
update_last_check_time,
)
class TestGetLastCheckFile:
"""Tests for get_last_check_file function."""
def test_returns_correct_path(self, tmp_path, monkeypatch):
"""Test that get_last_check_file returns correct path."""
# Mock Config.get_config_dir to return tmp_path
from dropkit.config import Config
monkeypatch.setattr(Config, "get_config_dir", lambda: tmp_path)
result = get_last_check_file()
assert result == tmp_path / ".last_version_check"
assert result.name == ".last_version_check"
class TestShouldCheckVersion:
"""Tests for should_check_version function."""
def test_no_file_returns_true(self, tmp_path, monkeypatch):
"""Test returns True when check file doesn't exist."""
from dropkit.config import Config
monkeypatch.setattr(Config, "get_config_dir", lambda: tmp_path)
result = should_check_version()
assert result is True
def test_expired_check_returns_true(self, tmp_path, monkeypatch):
"""Test returns True when more than 24 hours have passed."""
from dropkit.config import Config
monkeypatch.setattr(Config, "get_config_dir", lambda: tmp_path)
# Create check file with old timestamp (25 hours ago)
check_file = tmp_path / ".last_version_check"
old_timestamp = time.time() - (25 * 3600) # 25 hours ago
check_file.write_text(json.dumps({"timestamp": old_timestamp}))
result = should_check_version()
assert result is True
def test_not_expired_returns_false(self, tmp_path, monkeypatch):
"""Test returns False when less than 24 hours have passed."""
from dropkit.config import Config
monkeypatch.setattr(Config, "get_config_dir", lambda: tmp_path)
# Create check file with recent timestamp (1 hour ago)
check_file = tmp_path / ".last_version_check"
recent_timestamp = time.time() - 3600 # 1 hour ago
check_file.write_text(json.dumps({"timestamp": recent_timestamp}))
result = should_check_version()
assert result is False
def test_corrupted_file_returns_true(self, tmp_path, monkeypatch):
"""Test returns True when file contains invalid JSON."""
from dropkit.config import Config
monkeypatch.setattr(Config, "get_config_dir", lambda: tmp_path)
# Create check file with invalid JSON
check_file = tmp_path / ".last_version_check"
check_file.write_text("not valid json {{{")
result = should_check_version()
assert result is True
def test_missing_timestamp_returns_true(self, tmp_path, monkeypatch):
"""Test returns True when file missing timestamp key."""
from dropkit.config import Config
monkeypatch.setattr(Config, "get_config_dir", lambda: tmp_path)
# Create check file without timestamp
check_file = tmp_path / ".last_version_check"
check_file.write_text(json.dumps({"other_key": "value"}))
result = should_check_version()
# Should return True because timestamp defaults to 0, making it very old
assert result is True
class TestUpdateLastCheckTime:
"""Tests for update_last_check_time function."""
def test_creates_file_with_correct_structure(self, tmp_path, monkeypatch):
"""Test creates file with timestamp and current_version."""
from dropkit.config import Config
monkeypatch.setattr(Config, "get_config_dir", lambda: tmp_path)
before_time = time.time()
update_last_check_time()
after_time = time.time()
check_file = tmp_path / ".last_version_check"
assert check_file.exists()
data = json.loads(check_file.read_text())
assert "timestamp" in data
assert "current_version" in data
# Verify timestamp is recent (within the test execution window)
assert before_time <= data["timestamp"] <= after_time
def test_creates_parent_directory(self, tmp_path, monkeypatch):
"""Test creates parent directory if it doesn't exist."""
from dropkit.config import Config
nested_path = tmp_path / "nested" / "config"
monkeypatch.setattr(Config, "get_config_dir", lambda: nested_path)
update_last_check_time()
check_file = nested_path / ".last_version_check"
assert check_file.exists()
assert check_file.parent == nested_path
def test_silent_failure_on_permission_error(self, tmp_path, monkeypatch):
"""Test silently fails if can't write file."""
from dropkit.config import Config
monkeypatch.setattr(Config, "get_config_dir", lambda: tmp_path)
# Mock open to raise OSError
with patch("builtins.open", side_effect=OSError("Permission denied")):
# Should not raise exception
update_last_check_time()
class TestExtractCommitFromVersion:
"""Tests for extract_commit_from_version function."""
def test_format_git_dot(self):
"""Test extraction from '0.1.0+git.abc1234' format."""
result = extract_commit_from_version("0.1.0+git.abc1234")
assert result == "abc1234"
def test_format_plus_only(self):
"""Test extraction from '0.1.0+abc1234' format."""
result = extract_commit_from_version("0.1.0+abc1234")
assert result == "abc1234"
def test_truncates_long_hash(self):
"""Test truncates hash to 7 characters."""
result = extract_commit_from_version("0.1.0+git.abc1234567890")
assert result == "abc1234"
assert len(result) == 7
def test_dev_version_returns_none(self):
"""Test returns None for 'dev' version."""
result = extract_commit_from_version("dev")
assert result is None
def test_no_commit_returns_none(self):
"""Test returns None for version without commit."""
result = extract_commit_from_version("0.1.0")
assert result is None
def test_empty_string_returns_none(self):
"""Test returns None for empty string."""
result = extract_commit_from_version("")
assert result is None
def test_format_with_dots_in_suffix(self):
"""Test extraction when suffix contains dots."""
result = extract_commit_from_version("0.1.0+dev.123.abc1234")
# Should extract the last part after splitting by '.'
assert result == "abc1234"
class TestCommitsDiffer:
"""Tests for commits_differ function."""
def test_same_commit_returns_false(self):
"""Test returns False when commits match."""
result = commits_differ("0.1.0+git.abc1234", "abc1234")
assert result is False
def test_different_commits_returns_true(self):
"""Test returns True when commits differ."""
result = commits_differ("0.1.0+git.abc1234", "xyz9876")
assert result is True
def test_no_current_commit_returns_false(self):
"""Test returns False when current version has no commit."""
result = commits_differ("0.1.0", "abc1234")
assert result is False
def test_case_insensitive_comparison(self):
"""Test comparison is case-insensitive."""
result1 = commits_differ("0.1.0+git.ABC1234", "abc1234")
result2 = commits_differ("0.1.0+git.abc1234", "ABC1234")
assert result1 is False
assert result2 is False
def test_dev_version_returns_false(self):
"""Test returns False for dev version."""
result = commits_differ("dev", "abc1234")
assert result is False
class TestGetLatestGitCommit:
"""Tests for get_latest_git_commit function."""
@patch("subprocess.run")
def test_success_returns_short_hash(self, mock_run):
"""Test successful fetch returns 7-char hash."""
# Mock successful git ls-remote
mock_result = Mock()
mock_result.returncode = 0
mock_result.stdout = "abc1234567890abcdef1234567890abcdef1234\tHEAD\n"
mock_run.return_value = mock_result
result = get_latest_git_commit()
assert result == "abc1234"
assert len(result) == 7
@patch("subprocess.run")
def test_nonzero_returncode_returns_none(self, mock_run):
"""Test non-zero return code returns None."""
mock_result = Mock()
mock_result.returncode = 1
mock_result.stdout = ""
mock_run.return_value = mock_result
result = get_latest_git_commit()
assert result is None
@patch("subprocess.run")
def test_empty_output_returns_none(self, mock_run):
"""Test empty stdout returns None."""
mock_result = Mock()
mock_result.returncode = 0
mock_result.stdout = ""
mock_run.return_value = mock_result
result = get_latest_git_commit()
assert result is None
@patch("subprocess.run")
def test_timeout_returns_none(self, mock_run):
"""Test timeout returns None."""
mock_run.side_effect = subprocess.TimeoutExpired(cmd="git", timeout=5)
result = get_latest_git_commit()
assert result is None
@patch("subprocess.run")
def test_subprocess_error_returns_none(self, mock_run):
"""Test subprocess error returns None."""
mock_run.side_effect = subprocess.SubprocessError("Command failed")
result = get_latest_git_commit()
assert result is None
@patch("subprocess.run")
def test_os_error_returns_none(self, mock_run):
"""Test OS error returns None."""
mock_run.side_effect = OSError("Git not found")
result = get_latest_git_commit()
assert result is None
@patch("subprocess.run")
def test_calls_git_with_correct_args(self, mock_run):
"""Test calls git ls-remote with correct arguments."""
mock_result = Mock()
mock_result.returncode = 0
mock_result.stdout = "abc1234567890\tHEAD\n"
mock_run.return_value = mock_result
get_latest_git_commit()
mock_run.assert_called_once()
call_args = mock_run.call_args[0][0]
assert call_args == [
"git",
"ls-remote",
"https://github.com/trailofbits/dropkit.git",
"HEAD",
]