mirror of
https://github.com/trailofbits/dropkit
synced 2026-06-21 14:11:54 +00:00
314 lines
10 KiB
Python
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",
|
|
]
|