Files

1194 lines
55 KiB
Python

import json
import os
import pytest
from datetime import datetime, timedelta, timezone
from unittest import mock
from unittest.mock import Mock, patch
# Must import this before any SeedSigner imports
from base import BaseTest
from seedsigner.helpers.version import Version, VersionUtils, NotAllowedInSeedSignerOS, not_allowed_in_seedsigner_os
from seedsigner.models.settings import Settings
# overrides
TEST__VERSIONFILE__FILENAME = "version-test.json"
TEST__DOT_GIT_DIR_NAME = f"dot-git-test"
# Reusable test data
TEST__VERSION_NAME = "1.2.3" # Will require VersionUtils._prefix_version_name when verifying results
TEST__VERSION_FORK = "some_repo_owner"
TEST__VERSION_TIMESTAMP = datetime.now().astimezone(timezone.utc).replace(tzinfo=None)
TEST__VERSION_BRANCH = "some_test_branch"
TEST__VERSION_TAG = "some_test_tag"
TEST__SEMANTIC_TAG = "1.2.3-rc1"
TEST__SHORT_COMMIT_HASH = "c5efda3"
TEST__FULL_COMMIT_HASH = "c5efda306c60877191013a6093d92cd0bfcccec8"
TEST__VERSION_DICT = {
VersionUtils.VERSIONFILE_ATTR__NAME: VersionUtils._prefix_version_name(TEST__VERSION_NAME),
VersionUtils.VERSIONFILE_ATTR__FORK: TEST__VERSION_FORK,
VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: TEST__SHORT_COMMIT_HASH,
VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: TEST__VERSION_TIMESTAMP.isoformat(),
}
# Mimic result of reading from version.json
TEST__VERSION_FILE_CONTENTS = str(TEST__VERSION_DICT).replace("'", '"') # JSON uses double quotes
class VersionBaseTest(BaseTest):
""" Sets up test-specific overrides and reusable methods and fixtures. """
@pytest.fixture(autouse=True, scope="class")
def mock_versionfile_filename(self):
"""
Every test in this class (and subclasses) will automatically run with this patch
applied (autouse=True), but the patch will not persist beyond the test class.
"""
with patch.object(VersionUtils, 'VERSIONFILE__FILENAME', TEST__VERSIONFILE__FILENAME):
yield
@pytest.fixture(autouse=True)
def mock_DOT_GIT_DIR_NAME(self):
"""
Patch out the DOT_GIT_DIR_NAME to facilitate testing git-related methods
without touching the real filesystem.
"""
with patch.object(VersionUtils, 'DOT_GIT_DIR_NAME', TEST__DOT_GIT_DIR_NAME):
yield
@pytest.fixture(autouse=True)
def mock_GITHUB_ACTIONS__IS_CI(self):
"""
Patch out the GITHUB_ACTIONS__IS_CI env var so that tests operate under the same
assumptions when running locally and when actually running in CI. Any test that
needs to simulate being in CI can override this env var as needed.
"""
with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "false"}):
yield
@pytest.fixture(autouse=True)
def mock_popen(self):
"""
Prevent any os.popen calls from actually executing during tests.
"""
with patch("os.popen", autospec=True) as mock_popen:
# Default to returning an empty string for `read()`
mock_popen.return_value.read.return_value = ""
yield mock_popen
@classmethod
def write_test_version_file(cls):
"""
Write the test version file to disk.
"""
assert VersionUtils.VERSIONFILE__FILENAME == TEST__VERSIONFILE__FILENAME
with open(VersionUtils._get_version_file_path(), "w") as f:
f.write(TEST__VERSION_FILE_CONTENTS)
@classmethod
def delete_test_version_file(cls):
"""
Delete the test version file from disk.
"""
assert VersionUtils.VERSIONFILE__FILENAME == TEST__VERSIONFILE__FILENAME
try:
os.remove(VersionUtils._get_version_file_path())
except FileNotFoundError:
pass
def reset_version_singleton(self):
Version._instance = None
def setup_method(self):
super().setup_method()
def teardown_method(self):
super().teardown_method()
# Clean up any test version file created
self.delete_test_version_file()
self.reset_version_singleton()
class TestVersionBaseTest(VersionBaseTest):
def test_setup_and_teardown(self):
"""
Ensure that the setup and teardown methods work as expected.
"""
assert VersionUtils.VERSIONFILE__FILENAME == TEST__VERSIONFILE__FILENAME
# During setup, the test version file should not exist
assert not os.path.exists(VersionUtils._get_version_file_path())
# Write the test version file
self.write_test_version_file()
assert os.path.exists(VersionUtils._get_version_file_path())
# Delete should remove it
self.delete_test_version_file()
assert not os.path.exists(VersionUtils._get_version_file_path())
# Teardown should also delete the test version file
self.write_test_version_file()
self.teardown_method()
assert not os.path.exists(VersionUtils._get_version_file_path())
def test_mock_popen(self):
""" All os.popen calls should be automatically/invisibly mocked out. """
result = os.popen("echo 'Hello, World!'")
assert isinstance(result, mock.MagicMock)
class TestVersionUtils_BasicCalls(VersionBaseTest):
def test__prefix_version_name(self):
"""
Semantic versions should be prefixed with 'v' but others should be left as-is.
"""
# Expected to end up starting with "v":
for version_name in [
"1.2.3",
"v1.2.3",
"1.2.3-rc1",
"v1.2.3-rc1",
"21.known.slight.flaw",
]:
result = VersionUtils._prefix_version_name(version_name)
assert result.startswith("v"), f"Expected '{result}' to start with 'v'"
if version_name.startswith("v"):
assert result == version_name, f"Expected '{result}' to equal input '{version_name}'"
# Expect no change:
for version_name in [
"release-branch",
"feature/foo",
"hotfix-123",
"foo.bar.not.semantic",
"1234abcd",
"1234",
]:
result = VersionUtils._prefix_version_name(version_name)
assert result == version_name, f"Expected '{result}' to equal input '{version_name}'"
def test_get_version_name__not_detected(self, mock_popen: Mock):
"""
The possible success scenarios are tested elsewhere. In order to get to 100%
coverage we just need this basic test to hit the total failure condition.
"""
# If we don't provide the SeedSigner OS or Github Actions CI env vars, mock out
# the `git` shell commands via `mock_popen`, and mock out all .git/ file parsing,
# then the method should return the fallback warning note as the version name.
with patch("builtins.open", side_effect=FileNotFoundError):
result = VersionUtils.get_version_name()
assert "not detected" in result
def test_get_version_fork__not_detected(self, mock_popen: Mock):
"""
The possible success scenarios are tested elsewhere. In order to get to 100%
coverage we just need this basic test to hit the total failure condition.
"""
# If we don't provide the SeedSigner OS or Github Actions CI env vars, mock out
# the `git` shell commands via `mock_popen`, and mock out all .git/ file parsing,
# then the method should return None.
with patch("builtins.open", side_effect=FileNotFoundError):
VersionUtils.get_version_fork() is None
def test_get_short_commit_hash__not_detected(self, mock_popen: Mock):
"""
The possible success scenarios are tested elsewhere. In order to get to 100%
coverage we just need this basic test to hit the total failure condition.
"""
# If we don't provide the SeedSigner OS or Github Actions CI env vars, mock out
# the `git` shell commands via `mock_popen`, and mock out all .git/ file parsing,
# then the method should return None.
with patch("builtins.open", side_effect=FileNotFoundError):
VersionUtils.get_short_commit_hash() is None
class TestVersionUtils_VersionFile(VersionBaseTest):
def test_seedsigner_os_reads_from_version_file(self):
"""
Test the high-level basic public calls. Does just the minimal necessary mocking
since all the downstream helper methods are tested in detail elsewhere.
When running on SeedSigner OS, the version data should be read from the
version.json file.
"""
self.write_test_version_file()
# Simulate running on SeedSigner OS
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
assert VersionUtils.get_version_name() == VersionUtils._prefix_version_name(TEST__VERSION_NAME)
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# Expect errors if keys are missing from version.json
with patch("builtins.open", mock.mock_open(read_data="{'some_key':'some_value'}")):
with pytest.raises(Exception):
VersionUtils.get_version_name()
# This time it's the timestamp that's missing
with patch("builtins.open", mock.mock_open(read_data=str({VersionUtils.VERSIONFILE_ATTR__NAME: TEST__VERSION_NAME}).replace("'", '"'))):
with pytest.raises(Exception):
VersionUtils.get_version_timestamp()
def test__read_version_file(self):
"""
Low-level test for reading the version.json file.
Note that the version.json file will already "v" prefix version_name as needed.
"""
self.write_test_version_file()
version_data = VersionUtils._read_version_file()
assert version_data is not None
assert version_data[VersionUtils.VERSIONFILE_ATTR__NAME] == VersionUtils._prefix_version_name(TEST__VERSION_NAME)
assert version_data[VersionUtils.VERSIONFILE_ATTR__FORK] == TEST__VERSION_FORK
assert version_data[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH] == TEST__SHORT_COMMIT_HASH
assert version_data[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP] == TEST__VERSION_TIMESTAMP.isoformat()
def test__read_version_file__missing(self):
""" _read_version_file should return None if the version file is missing. """
assert os.path.exists(VersionUtils._get_version_file_path()) is False
assert VersionUtils._read_version_file() is None
# The upstream dependent calls should also return None
assert VersionUtils._get_version_name_from_version_file() is None
assert VersionUtils._get_version_fork_from_version_file() is None
assert VersionUtils._get_short_commit_hash_from_version_file() is None
assert VersionUtils._get_version_timestamp_from_version_file() is None
# Gracefully handle other unexpected exceptions
with patch("builtins.open", side_effect=Exception("Unexpected error")):
assert VersionUtils._read_version_file() is None
class TestVersionUtils_GithubActions(VersionBaseTest):
def test_github_actions_env_vars(self):
"""
Test the high-level basic public calls. Does just the minimal necessary mocking
since all the downstream helper methods are tested in detail elsewhere.
When running in a GitHub Actions CI environment, the version data should be
pulled from the appropriate env vars.
"""
# CI uses some limited `git` shell calls; mock out the associated calls.
with patch("seedsigner.helpers.version.VersionUtils._get_version_timestamp_from_git_shell", return_value=TEST__VERSION_TIMESTAMP):
random_sha = "abcd1234ef567890abcd1234ef567890abcd1234"
# When running CI on a branch
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__HEAD_REF: TEST__VERSION_BRANCH,
VersionUtils.ENV_VAR__GITHUB_ACTIONS__PR_AUTHOR: TEST__VERSION_FORK,
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA: TEST__FULL_COMMIT_HASH,
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: random_sha,
}):
assert VersionUtils.get_version_name() == TEST__VERSION_BRANCH
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# When running CI on a semantic tag
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__HEAD_REF: TEST__SEMANTIC_TAG,
}):
# Should be prefixed with "v"
assert VersionUtils.get_version_name() == f"v{TEST__SEMANTIC_TAG}"
# When running CI on a generic tag
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__HEAD_REF: TEST__VERSION_TAG,
}):
# Should NOT be prefixed with "v"
assert VersionUtils.get_version_name() == TEST__VERSION_TAG
# When running CI on a commit (detached HEAD) with no HEAD_REF or REF_NAME,
# the version_name should be the short commit hash.
# TODO: I don't think this scenario ever happens.
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__HEAD_REF: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA: TEST__FULL_COMMIT_HASH,
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: random_sha,
}):
assert VersionUtils.get_version_name() == TEST__SHORT_COMMIT_HASH
# When running CI on a commit (detached HEAD) with no HEAD_REF or REF_NAME and
# no SOURCE_SHA or SHA, raise error.
# Note: This scenario definitely would never happen. Just trying to get to
# 100% test coverage.
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__HEAD_REF: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: "",
}):
with pytest.raises(Exception):
VersionUtils.get_version_name()
# Github Actions CI logic should gracefully handle missing SHA data, even
# if it's an impossible / unlikely scenario.
assert VersionUtils.get_short_commit_hash() is None
def test_is_github_actions_ci(self):
"""
is_github_actions_ci should return True only when the
ENV_VAR__GITHUB_ACTIONS__IS_CI env var is set to "true".
Note: Have to mock a value for ALL scenario variations here because this test will
actually run in Github Actions so the env var will already be present!
"""
with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true"}):
assert VersionUtils.is_github_actions_ci() is True
with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "false"}):
assert VersionUtils.is_github_actions_ci() is False
with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "1"}):
assert VersionUtils.is_github_actions_ci() is False
def test__get_version_name_from_github_actions_env_vars(self):
"""
_get_version_name_from_github_actions_env_vars should prioritize HEAD_REF, then
REF_NAME, then SHA env vars when set.
"""
test_head_ref = "head_ref"
test_ref_name = "ref_name"
test_sha = TEST__FULL_COMMIT_HASH
random_sha = "abcd1234ef567890abcd1234ef567890abcd1234"
# Need to signal that we're in a GitHub Actions CI environment
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__HEAD_REF: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: "",
}):
# With none of the env vars set, should return None
assert VersionUtils._get_version_name_from_github_actions_env_vars() is None
# HEAD_REF should be passed straight through, ignoring other vars
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__HEAD_REF: test_head_ref,
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: test_ref_name,
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA: test_sha,
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: random_sha,
}):
result = VersionUtils._get_version_name_from_github_actions_env_vars()
assert result == test_head_ref
# REF_NAME is our next fallback
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__HEAD_REF: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: test_ref_name,
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA: test_sha,
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: random_sha,
}):
result = VersionUtils._get_version_name_from_github_actions_env_vars()
assert result == test_ref_name
# Unlikely scenario: no HEAD_REF nor REF_NAME but SOURCE_SHA is set; should return
# short commit hash.
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__HEAD_REF: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA: test_sha,
}):
result = VersionUtils._get_version_name_from_github_actions_env_vars()
assert result == test_sha[:7]
# Unlikely scenario 2: no HEAD_REF nor REF_NAME and no SOURCE_SHA, but SHA is
# set; should return short commit hash.
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__HEAD_REF: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA: test_sha,
}):
result = VersionUtils._get_version_name_from_github_actions_env_vars()
assert result == test_sha[:7]
def test__get_version_fork_from_github_actions_env_vars(self):
"""
_get_version_fork_from_github_actions_env_vars should return the PR_AUTHOR, then
fall back to REPOSITORY_OWNER.
"""
test_pr_author = "some_pr_author"
test_repo_owner = "some_repo_owner"
# Need to signal that we're in a GitHub Actions CI environment
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__PR_AUTHOR: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REPOSITORY_OWNER: "",
}):
# With none of the env vars set, should return None
assert VersionUtils._get_version_fork_from_github_actions_env_vars() is None
# PR_AUTHOR should be passed straight through, ignoring REPOSITORY_OWNER
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__PR_AUTHOR: test_pr_author,
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REPOSITORY_OWNER: test_repo_owner,
}):
result = VersionUtils._get_version_fork_from_github_actions_env_vars()
assert result == test_pr_author
# REPOSITORY_OWNER is our next fallback
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__PR_AUTHOR: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REPOSITORY_OWNER: test_repo_owner,
}):
result = VersionUtils._get_version_fork_from_github_actions_env_vars()
assert result == test_repo_owner
class TestVersionUtils_SeedSignerOSBuilder(VersionBaseTest):
def test_seed_signer_os_builder_env_var(self):
"""
Test the high-level basic public calls. Does just the minimal necessary mocking
since all the downstream helper methods are tested in detail elsewhere.
"""
# Simulate running in the SeedSigner OS build environment
with patch.dict(os.environ, {VersionUtils.ENV_VAR__IS_SEEDSIGNER_OS_BUILDER: "1"}):
# SeedSigner OS builder uses some limited `git` shell calls; mock out the
# associated calls.
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_get_version_name_from_git_shell=Mock(return_value=TEST__VERSION_NAME),
_get_version_fork_from_git_shell=Mock(return_value=TEST__VERSION_FORK),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
_get_version_timestamp_from_git_shell=Mock(return_value=TEST__VERSION_TIMESTAMP),
):
# Meanwhile, mock out the timestamp from source files and ensure it's NOT used
with patch.object(
VersionUtils,
"_get_last_modified_timestamp_from_src_files",
Mock(return_value=datetime.now())
):
assert VersionUtils.get_version_name() == VersionUtils._prefix_version_name(TEST__VERSION_NAME)
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# Verify that the source file timestamp method was NOT called
VersionUtils._get_last_modified_timestamp_from_src_files.assert_not_called()
def test_is_seedsigner_os_builder_env(self):
"""
is_seedsigner_os_builder_env should return True only when the
ENV_VAR__IS_SEEDSIGNER_OS_BUILDER env var is set.
"""
with patch.dict(os.environ, {}, clear=True):
assert VersionUtils._is_seedsigner_os_builder_env() is False
with patch.dict(os.environ, {VersionUtils.ENV_VAR__IS_SEEDSIGNER_OS_BUILDER: "1"}):
assert VersionUtils._is_seedsigner_os_builder_env() is True
class TestVersionUtils_GitShell(VersionBaseTest):
def test_local_dev_with_git_shell_calls(self):
"""
Test the high-level basic public calls. Does just the minimal necessary mocking
since all the downstream helper methods are tested in detail elsewhere.
When running in a local dev environment with access to `git` shell commands,
the version data should be pulled from those commands.
Note that in local dev we use the last modified timestamp from the source files
rather than from git.
"""
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_get_version_name_from_git_shell=Mock(return_value=TEST__VERSION_BRANCH),
_get_version_fork_from_git_shell=Mock(return_value=TEST__VERSION_FORK),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__SHORT_COMMIT_HASH),
_get_last_modified_timestamp_from_src_files=Mock(return_value=TEST__VERSION_TIMESTAMP),
):
# Mock out the git shell call that retrieves the timestamp.
with patch(
"seedsigner.helpers.version.VersionUtils._get_version_timestamp_from_git_shell",
return_value=None
) as mock_timestamp_from_git_shell:
# Verify that we're using the local dev code path
assert VersionUtils.is_github_actions_ci() is False
assert VersionUtils._is_seedsigner_os_builder_env() is False
assert VersionUtils.get_version_name() == TEST__VERSION_BRANCH
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# Verify that the git shell timestamp method was NOT called
mock_timestamp_from_git_shell.assert_not_called()
def test__get_version_name_from_git_shell(self):
"""
Test that _get_version_name_from_git_shell returns the expected name depending on
the current git state
"""
# Default mock_popen return empty string; simulates no `git` shell command available
# or no local git data.
result = VersionUtils._get_version_name_from_git_shell()
assert result is None
# If we're on a branch, should return the branch name
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_get_branch_name_from_git_shell=Mock(return_value=TEST__VERSION_BRANCH),
_get_tag_name_from_git_shell=Mock(return_value=TEST__VERSION_TAG),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
):
result = VersionUtils._get_version_name_from_git_shell()
assert result == TEST__VERSION_BRANCH
# If we're on a tag, the detached HEAD state wipes out the branch name
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_get_branch_name_from_git_shell=Mock(return_value=None),
_get_tag_name_from_git_shell=Mock(return_value=TEST__VERSION_TAG),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
):
result = VersionUtils._get_version_name_from_git_shell()
assert result == TEST__VERSION_TAG
# Similarly, if we're detached at a specific commit hash.
# Note: when falling back to the commit hash, we're expecting the SHORT commit
# hash.
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_get_branch_name_from_git_shell=Mock(return_value=None),
_get_tag_name_from_git_shell=Mock(return_value=None),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
):
result = VersionUtils._get_version_name_from_git_shell()
assert result == TEST__SHORT_COMMIT_HASH
def test__get_version_fork_from_git_shell(self, mock_popen: Mock):
"""
Test that _get_version_fork_from_git_shell returns the expected repo owner from
the remote url.
"""
for owner in ["SeedSigner", "seedsigner", "some-user"]:
remote_url = f"https://github.com/{owner}/seedsigner.git"
mock_popen.return_value.read.return_value = remote_url
result = VersionUtils._get_version_fork_from_git_shell()
assert result == owner
def test__get_full_commit_hash_from_git_shell(self, mock_popen: Mock):
"""
Test that _get_full_commit_hash_from_git_shell returns the expected full commit hash.
"""
mock_popen.return_value.read.return_value = TEST__FULL_COMMIT_HASH
result = VersionUtils._get_full_commit_hash_from_git_shell()
assert result == TEST__FULL_COMMIT_HASH
def test__get_short_commit_hash_from_git_shell(self, mock_popen: Mock):
"""
Test that _get_short_commit_hash_from_git_shell returns the expected short commit hash.
"""
mock_popen.return_value.read.return_value = TEST__FULL_COMMIT_HASH
result = VersionUtils._get_short_commit_hash_from_git_shell()
assert result == TEST__SHORT_COMMIT_HASH
def test__get_version_timestamp_from_git_shell(self, mock_popen: Mock):
"""
Test that _get_version_timestamp_from_git_shell returns the expected datetime.
"""
# Should gracefully handle no `git` shell command available or no local git data.
mock_popen.return_value.read.return_value = ""
assert VersionUtils._get_version_timestamp_from_git_shell() is None
# Initial timestamp has timezone info
hour = 14
tz_offset = 1
test_local_isoformat = f"2025-12-20T{hour:02}:00:00-{tz_offset:02}:00"
mock_popen.return_value.read.return_value = test_local_isoformat
# But the final result will be UTC
expected_datetime = datetime.fromisoformat(f"2025-12-20T{hour + tz_offset:02}:00:00")
assert VersionUtils._get_version_timestamp_from_git_shell() == expected_datetime
# And UTC-to-UTC should be unchanged
mock_popen.return_value.read.return_value = expected_datetime.isoformat() + "+00:00"
assert VersionUtils._get_version_timestamp_from_git_shell() == expected_datetime
# Github Actions CI timestamps return "Z" format
expected_utc_isoformat = "2025-12-26T19:41:49"
github_format = expected_utc_isoformat + "Z"
mock_popen.return_value.read.return_value = github_format
expected_datetime = datetime.fromisoformat(expected_utc_isoformat)
assert VersionUtils._get_version_timestamp_from_git_shell() == expected_datetime
class TestVersionUtils_DotGitFiles(VersionBaseTest):
def test_local_dev_with_dot_git_dir_parsing(self, mock_popen: Mock):
"""
Test the high-level basic public calls. Does just the minimal necessary mocking
since all the downstream helper methods are tested in detail elsewhere.
When running in a local dev environment without access to `git` shell commands,
the version data should be pulled from parsing the .git directory.
We pull in the `mock_popen` fixture here to simulate `git` shell commands failing.
Note that in local dev we use the last modified timestamp from the source files
rather than from git.
"""
# If we're on a branch, getting the commit hash requires the
# .git/refs/heads/<branch> file.
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_read_git_HEAD_file=Mock(return_value=(TEST__VERSION_BRANCH, None)),
_get_full_commit_hash_from_git_refs_heads=Mock(return_value=TEST__FULL_COMMIT_HASH),
_get_version_fork_from_git_config=Mock(return_value=TEST__VERSION_FORK),
_get_last_modified_timestamp_from_src_files=Mock(return_value=TEST__VERSION_TIMESTAMP),
):
# Mock out the git shell call that retrieves the timestamp.
with patch(
"seedsigner.helpers.version.VersionUtils._get_version_timestamp_from_git_shell",
return_value=None
) as mock_timestamp_from_git_shell:
# Verify that we're using the local dev code path
assert VersionUtils.is_github_actions_ci() is False
assert VersionUtils._is_seedsigner_os_builder_env() is False
assert VersionUtils.get_version_name() == TEST__VERSION_BRANCH
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
# If we're on a tag (detached HEAD), getting the commit hash requires
# checking the refs/tags for a matching tag.
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_read_git_HEAD_file=Mock(return_value=(None, TEST__FULL_COMMIT_HASH)),
_get_matching_tag_from_git_refs_tags=Mock(return_value=TEST__VERSION_TAG),
):
assert VersionUtils.get_version_name() == TEST__VERSION_TAG
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
# If we're on a commit hash (detached HEAD) with no matching tag, the
# version name should be the short commit hash.
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_read_git_HEAD_file=Mock(return_value=(None, TEST__FULL_COMMIT_HASH)),
_get_matching_tag_from_git_refs_tags=Mock(return_value=None),
):
assert VersionUtils.get_version_name() == TEST__SHORT_COMMIT_HASH
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
# Verify that through all of this that the git shell timestamp method was
# NOT called.
mock_timestamp_from_git_shell.assert_not_called()
def test__get_dot_git_dir(self):
"""
_get_dot_git_dir should return the expected .git directory path.
"""
# The mocked DOT_GIT_DIR_NAME should be in the result
result = VersionUtils._get_dot_git_dir()
assert TEST__DOT_GIT_DIR_NAME in result
def test__read_git_HEAD_file(self):
"""
_read_git_HEAD_file should parse the git HEAD file to extract the current
branch name or commit hash. Or gracefully return None if the HEAD file is missing.
"""
# Initially our test setup has no .git dir
assert VersionUtils._read_git_HEAD_file() == (None, None)
# If we're on a branch...
git_HEAD_content = f"ref: refs/heads/{TEST__VERSION_BRANCH}"
with patch("builtins.open", mock.mock_open(read_data=git_HEAD_content)):
branch_name, commit_hash = VersionUtils._read_git_HEAD_file()
assert branch_name == TEST__VERSION_BRANCH
assert commit_hash is None
# If we're in a detached HEAD state at a specific commit hash...
with patch("builtins.open", mock.mock_open(read_data=TEST__FULL_COMMIT_HASH)):
branch_name, commit_hash = VersionUtils._read_git_HEAD_file()
assert branch_name is None
assert commit_hash == TEST__FULL_COMMIT_HASH
# Gracefully handle a read error
with patch("builtins.open", side_effect=FileNotFoundError):
assert VersionUtils._read_git_HEAD_file() == (None, None)
# Or any other kind of exception
with patch("builtins.open", side_effect=Exception("Unexpected error")):
assert VersionUtils._read_git_HEAD_file() == (None, None)
def test__get_matching_tag_from_git_refs_tags(self):
"""
_get_matching_tag_from_git_refs_tags should return the expected tag name
if a matching tag is found for the given commit hash.
"""
# No .git dir initially
assert os.path.exists(VersionUtils._get_dot_git_dir()) is False
# Mock the os.listdir of the .git/refs/tags directory to the target tag name
with patch("os.listdir", return_value=[TEST__VERSION_NAME]):
# Mock the open of the tag ref file to return our test commit hash
with patch("builtins.open", mock.mock_open(read_data=TEST__FULL_COMMIT_HASH)):
tag_name = VersionUtils._get_matching_tag_from_git_refs_tags(TEST__FULL_COMMIT_HASH)
assert tag_name == TEST__VERSION_NAME
# If no matching tag is found, should return None
with patch("os.listdir", return_value=["some_other_tag"]):
with patch("builtins.open", mock.mock_open(read_data="different_commit_hash")):
tag_name = VersionUtils._get_matching_tag_from_git_refs_tags(TEST__FULL_COMMIT_HASH)
assert tag_name is None
# If the refs/tags directory is missing, should return None
with patch("seedsigner.helpers.version.VersionUtils._get_dot_git_dir", return_value="/nonexistent/path"):
tag_name = VersionUtils._get_matching_tag_from_git_refs_tags(TEST__FULL_COMMIT_HASH)
assert tag_name is None
# If any other exception occurs, should return None
with patch("os.listdir", side_effect=Exception("Unexpected error")):
tag_name = VersionUtils._get_matching_tag_from_git_refs_tags(TEST__FULL_COMMIT_HASH)
assert tag_name is None
def test__get_version_name_from_git_HEAD(self):
# Should gracefully return None if there's no .git dir or .git/HEAD file.
assert VersionUtils._get_version_name_from_git_HEAD() == None
# We already tested _read_git_HEAD_file() above, so just mock scenarios here.
# On a branch:
with patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(TEST__VERSION_BRANCH, None)):
assert VersionUtils._get_version_name_from_git_HEAD() == TEST__VERSION_BRANCH
# Detached HEAD at commit hash, with matching tag
with patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__FULL_COMMIT_HASH)):
with patch.object(VersionUtils, '_get_matching_tag_from_git_refs_tags', return_value=TEST__VERSION_NAME):
assert VersionUtils._get_version_name_from_git_HEAD() == TEST__VERSION_NAME
# Detached HEAD at commit hash, no matching tag; returns the short commit hash
with patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__FULL_COMMIT_HASH)):
with patch.object(VersionUtils, '_get_matching_tag_from_git_refs_tags', return_value=None):
assert VersionUtils._get_version_name_from_git_HEAD() == TEST__SHORT_COMMIT_HASH
def test__get_commit_hash_from_git_HEAD(self):
# _get_commit_hash_from_git_HEAD is a trivial convenience function that relies on
# _read_git_HEAD_file which we've already tested. Just verify the expected outputs
# here.
with patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__SHORT_COMMIT_HASH)):
assert VersionUtils._get_full_commit_hash_from_git_HEAD() == TEST__SHORT_COMMIT_HASH
def test__get_full_commit_hash_from_git_refs_heads(self):
"""
_get_full_commit_hash_from_git_refs_heads should return the expected commit hash
for the given branch name.
"""
# No .git dir initially
assert os.path.exists(VersionUtils._get_dot_git_dir()) is False
assert VersionUtils._get_full_commit_hash_from_git_refs_heads(TEST__VERSION_BRANCH) is None
# Mock the open of the branch ref file to return our test commit hash
with patch("builtins.open", mock.mock_open(read_data=TEST__FULL_COMMIT_HASH)):
with patch("os.path.exists", return_value=True):
commit_hash = VersionUtils._get_full_commit_hash_from_git_refs_heads(TEST__VERSION_BRANCH)
assert commit_hash == TEST__FULL_COMMIT_HASH
# If the branch ref file is missing, should return None
with patch("os.path.exists", return_value=False):
commit_hash = VersionUtils._get_full_commit_hash_from_git_refs_heads(TEST__VERSION_BRANCH)
assert commit_hash is None
# If any other exception occurs, should return None
with patch("builtins.open", side_effect=Exception("Unexpected error")):
commit_hash = VersionUtils._get_full_commit_hash_from_git_refs_heads(TEST__VERSION_BRANCH)
assert commit_hash is None
def test__parse_git_remote_url(self):
"""
_parse_git_remote_url should return the expected repo owner from various
git remote url formats.
"""
# Might be called with no remote url. Should also handle unrecognized formats.
for url in [None, "", "what/is/this"]:
assert VersionUtils._parse_git_remote_url(url) is None
for url, expected_owner in [
("https://github.com/SeedSigner/seedsigner.git", "SeedSigner"),
("git@github.com:SeedSigner/seedsigner.git", "SeedSigner"),
("https://gitlab.com/some_user/some-repo.git", "some_user"),
("git@gitlab.com:other-user/some-repo.git", "other-user"),
]:
assert VersionUtils._parse_git_remote_url(url) == expected_owner
def test__get_version_fork_from_git_config(self):
"""
Test that _get_version_fork_from_git_config returns the expected repo owner from
the remote url.
"""
expected_fork = "SeedSigner"
remote_url = f"git@github.com:{expected_fork}/seedsigner.git"
git_config = f"""
[some_section]
some_key = some_value
[remote "origin"]
url = {remote_url}
fetch = +refs/heads/*:refs/remotes/origin/*
[next_section]
next_key = next_value
"""
with patch("builtins.open", mock.mock_open(read_data=git_config)):
assert VersionUtils._get_version_fork_from_git_config() == expected_fork
# Missing origin section should return None
git_config_no_origin = """
[some_section]
some_key = some_value
[next_section]
next_key = next_value
"""
with patch("builtins.open", mock.mock_open(read_data=git_config_no_origin)):
assert VersionUtils._get_version_fork_from_git_config() is None
# Handle malformed origin section (can't find url) should return None
git_config_malformed_origin = """
[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/*
[next_section]
next_key = next_value
"""
with patch("builtins.open", mock.mock_open(read_data=git_config_malformed_origin)):
assert VersionUtils._get_version_fork_from_git_config() is None
# If the git config file is missing, should return None
with patch("builtins.open", side_effect=FileNotFoundError):
assert VersionUtils._get_version_fork_from_git_config() is None
def test__get_last_modified_timestamp_from_src_files(self):
"""
_get_last_modified_timestamp_from_src_files should return the most recent file
modification timestamp from the SeedSigner python files.
"""
# Do the real filesystem scan
timestamp = VersionUtils._get_last_modified_timestamp_from_src_files()
# Should be more recent than the initial SeedSigner v0.0.1 commit
assert timestamp > datetime(2020, 12, 13)
# Can't be in the future
assert timestamp < datetime.now() + timedelta(days=30)
# Now mock out os.path.getmtime to force all files to have a known timestamp
local_timestamp = datetime(2025, 12, 23, 0, 0, 0)
with patch("os.path.getmtime", return_value=local_timestamp.timestamp()):
# Note that the result will be converted to UTC with no tzinfo
assert VersionUtils._get_last_modified_timestamp_from_src_files() == local_timestamp.astimezone(timezone.utc).replace(tzinfo=None)
# Mock out os.walk() to simulate no .py files found
with patch("os.walk", return_value=[]):
assert VersionUtils._get_last_modified_timestamp_from_src_files() is None
class TestVersionUtils_Misc(VersionBaseTest):
"""
Tests for any other remaining methods in VersionUtils.
"""
def test__fetch_latest_seedsigner_release_tag(self, mock_popen: Mock):
"""
Test that _fetch_latest_seedsigner_release_tag returns the expected version string.
"""
latest_releases_response_dict = {
"url": "https://api.github.com/repos/SeedSigner/seedsigner/releases/228915183",
"tag_name": "0.8.6",
"prerelease": False,
"created_at": "2025-06-22T01:38:41Z",
"updated_at": "2025-06-30T20:49:22Z",
"published_at": "2025-06-30T20:45:05Z"
}
fake_response = Mock()
fake_response.status = 200
fake_response.read.return_value = json.dumps(latest_releases_response_dict).encode('utf-8')
with patch("urllib.request.urlopen", return_value=fake_response):
release_tag, release_timestamp = VersionUtils._fetch_latest_seedsigner_release_tag()
# mock_popen returns empty string, which mimics not having local git data to
# retrieve the commit timestamp.
assert mock_popen.called is True
# So then we expect to fall back to using the published_at time from the API
expected_timestamp = datetime.fromisoformat(latest_releases_response_dict["published_at"].replace("Z", "+00:00")).replace(tzinfo=None)
assert release_tag == VersionUtils._prefix_version_name(latest_releases_response_dict["tag_name"])
assert release_timestamp == expected_timestamp
# Update mock_popen so that we CAN get the (simulated) commit timestamp from
# local git data.
git_timestamp = "2025-06-21T21:38:41-04:00"
mock_popen.return_value.read.return_value = f"{git_timestamp}\n"
expected_timestamp = datetime.fromisoformat(git_timestamp).astimezone(timezone.utc).replace(tzinfo=None)
release_tag, release_timestamp = VersionUtils._fetch_latest_seedsigner_release_tag()
assert release_tag == VersionUtils._prefix_version_name(latest_releases_response_dict["tag_name"])
assert release_timestamp == expected_timestamp
# Should gracefully handle HTTP errors
fake_error_response = Mock()
fake_error_response.status = 404
with patch("urllib.request.urlopen", return_value=fake_error_response):
release_tag, release_timestamp = VersionUtils._fetch_latest_seedsigner_release_tag()
assert release_tag is None
assert release_timestamp is None
# And other exceptions
with patch("urllib.request.urlopen", side_effect=Exception("Network error")):
release_tag, release_timestamp = VersionUtils._fetch_latest_seedsigner_release_tag()
assert release_tag is None
assert release_timestamp is None
class TestVersion(VersionBaseTest):
"""
`Version` is really just a way to store the version data; all the real work is done in
`VersionUtils` and all of those calls have already been fully covered by the above
tests. So there's nothing to really test here. Just providing minimal tests in order
to get full test coverage.
"""
def test_basic_calls(self):
self.write_test_version_file()
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
Version.get_version_name() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__NAME]
Version.get_version_fork() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__FORK]
Version.get_short_commit_hash() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH]
Version.get_version_timestamp() == TEST__VERSION_TIMESTAMP
def test_is_release_image(self):
"""
Should return True if the heuristic checks indicate this is a release image.
"""
self.write_test_version_file()
def change_version_data(changes_dict):
# Can only call override_data() when NOT in SeedSigner OS; temporarily patch
# it out.
with patch("seedsigner.models.settings.Settings.HOSTNAME", "not-seedsigner-os"):
Version.override_data(**changes_dict)
# Must be running in SeedSigner OS
with patch("seedsigner.models.settings.Settings.HOSTNAME", "not-seedsigner-os"):
assert Version.is_release_image() is False
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
# Not from the main repo
for fork_name in [
"some-fork",
"AnotherRepo",
"seedsigner-someone_else",
"seedsigner123"
]:
change_version_data({
VersionUtils.VERSIONFILE_ATTR__FORK: fork_name,
})
assert Version.is_release_image() is False
# Reset to the main repo
change_version_data({
VersionUtils.VERSIONFILE_ATTR__FORK: "SeedSigner",
})
# Non-semantic version names
for version_name in [
"some-branch-name",
"dev",
"name.with.dots",
"version_1_2_3",
"v1.hah.fooled.you",
"v1.2.3-rc1", # only fully "clean" semantic versions count as releases
]:
change_version_data({
VersionUtils.VERSIONFILE_ATTR__NAME: version_name,
})
assert Version.is_release_image() is False
# Semantic versions pass
for version_name in [
"v0.8.5",
"v1.0",
"v10.20.30",
]:
change_version_data({
VersionUtils.VERSIONFILE_ATTR__NAME: version_name,
})
assert Version.is_release_image() is True
def test_override_data(self, mock_popen: Mock):
"""
Test that we can override the version data via the Version.override_version_data()
method.
"""
# Initially the version data is pulled from the usual sources
self.write_test_version_file()
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
assert Version.get_version_name() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__NAME]
assert Version.get_version_fork() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__FORK]
assert Version.get_short_commit_hash() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH]
assert Version.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# While we're in the mocked SeedSigner OS environment, verify that the
# override is not allowed.
with pytest.raises(NotAllowedInSeedSignerOS):
Version.override_data()
override_name = "v9.9.9-test"
override_fork = "TestFork"
override_commit_hash = "abcd123"
override_timestamp = datetime(2030, 1, 1, 0, 0, 0)
override_dict = {
VersionUtils.VERSIONFILE_ATTR__NAME: override_name,
VersionUtils.VERSIONFILE_ATTR__FORK: override_fork,
VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: override_commit_hash,
VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: override_timestamp,
}
# No longer in the mocked SeedSigner OS environment; should be allowed now.
Version.override_data(**override_dict)
assert Version.get_version_name() == override_name
assert Version.get_version_fork() == override_fork
assert Version.get_short_commit_hash() == override_commit_hash
assert Version.get_version_timestamp() == override_timestamp
# No overrides specified should result in no values changed.
Version.override_data()
assert Version.get_version_name() == override_name
assert Version.get_version_fork() == override_fork
assert Version.get_short_commit_hash() == override_commit_hash
assert Version.get_version_timestamp() == override_timestamp
def test_to_dict(self):
"""
Test that Version.to_dict() returns the expected dictionary.
"""
self.write_test_version_file()
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
# In SeedSigner OS, the version data comes from the version file.
assert Version.to_dict() == TEST__VERSION_DICT
extra_attr = "extra"
different_dict = {
VersionUtils.VERSIONFILE_ATTR__NAME: "v1.2.3-different",
VersionUtils.VERSIONFILE_ATTR__FORK: "DifferentFork",
VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: "differenthash",
VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: datetime(2026, 1, 1, 0, 0, 0), # stored internally as a datetime
extra_attr: "should be ignored",
}
Version.override_data(**different_dict)
output_dict = Version.to_dict()
assert output_dict[VersionUtils.VERSIONFILE_ATTR__NAME] == different_dict[VersionUtils.VERSIONFILE_ATTR__NAME]
assert output_dict[VersionUtils.VERSIONFILE_ATTR__FORK] == different_dict[VersionUtils.VERSIONFILE_ATTR__FORK]
assert output_dict[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH] == different_dict[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH]
assert output_dict[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP] == different_dict[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP].isoformat() # returned as a string
assert extra_attr not in output_dict
class TestNotAllowedInSeedSignerOSDecorator(BaseTest):
SUCCESS = "success"
@not_allowed_in_seedsigner_os
def dummy_function(self):
return self.SUCCESS
def test_not_allowed_in_seedsigner_os(self):
"""
The not_allowed_in_seedsigner_os decorator should raise its associated exception
if we run a decorated function while in SeedSigner OS.
"""
# Patch over the Settings.HOSTNAME value to simulate running in SeedSigner OS
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
with pytest.raises(NotAllowedInSeedSignerOS):
self.dummy_function()
def test_allowed_outside_seedsigner_os(self):
# Now try with any other HOSTNAME
with patch("seedsigner.models.settings.Settings.HOSTNAME", "my_dev_machine"):
assert self.dummy_function() == self.SUCCESS
class TestNotVersionBaseTest(BaseTest):
"""
Sanity check to verify that VersionBaseTest mocks do not affect other tests.
"""
def test_version_file_name__not_patched(self):
"""
Ensure that outside of VersionBaseTest, the VERSIONFILE__FILENAME patch does not
persist.
"""
assert VersionUtils.VERSIONFILE__FILENAME != TEST__VERSIONFILE__FILENAME
def test_mock_popen__not_patched(self):
"""
Ensure that os.popen is not mocked outside of VersionBaseTest.
"""
result = os.popen("echo 'Hello, World!'")
assert not isinstance(result, mock.MagicMock)