import os
import tempfile
import pytest
from unittest.mock import Mock, patch, mock_open
from secretsmith.vault.client import from_config, resolve_token, resolve_namespace, login_with_approle
class TestFromConfig:
"""Test the from_config method"""
@patch('secretsmith.vault.client.Client')
@patch('secretsmith.vault.client.login_with_approle')
def test_from_config_approle_method(self, mock_login_approle, mock_client):
"""Test that approle authentication calls the login method"""
# Arrange
config = {
"server": {"url": "https://vault.example.com"},
"auth": {
"method": "approle",
"role_id": "test-role-id",
"secret_id": "test-secret-id"
}
}
mock_client_instance = Mock()
mock_client.return_value = mock_client_instance
# Act
result = from_config(config)
# Assert
mock_client.assert_called_once_with(
url="https://vault.example.com",
token=None,
verify=None,
namespace=None
)
mock_login_approle.assert_called_once_with(mock_client_instance, config["auth"])
assert result == mock_client_instance
@patch('secretsmith.vault.client.Client')
def test_from_config_token_method(self, mock_client):
"""Test that token authentication works without calling approle login"""
# Arrange
config = {
"server": {"url": "https://vault.example.com"},
"auth": {
"method": "token",
"token": "s.test-token"
}
}
mock_client_instance = Mock()
mock_client.return_value = mock_client_instance
# Act
result = from_config(config)
# Assert
mock_client.assert_called_once_with(
url="https://vault.example.com",
token="s.test-token",
verify=None,
namespace=None
)
assert result == mock_client_instance
@patch('secretsmith.vault.client.Client')
def test_from_config_unknown_method_raises_error(self, mock_client):
"""Test that unknown authentication method raises ValueError"""
# Arrange
config = {
"auth": {
"method": "notexisting"
}
}
mock_client_instance = Mock()
mock_client.return_value = mock_client_instance
# Act & Assert
with pytest.raises(ValueError, match="Unknown auth method: notexisting"):
from_config(config)
class TestResolveToken:
"""Test the resolve_token function"""
def test_resolve_token_no_config_returns_none(self):
"""Test that empty config returns None"""
# Arrange
config_auth = {}
# Act
result = resolve_token(config_auth)
# Assert
assert result is None
def test_resolve_token_from_file(self):
"""Test reading token from file"""
# Arrange
token_content = "s.test-file-token"
with tempfile.NamedTemporaryFile(mode='w', delete=False, dir='tests') as temp_file:
temp_file.write(token_content + "\n ") # Add whitespace to test strip()
temp_file_path = temp_file.name
try:
config_auth = {"tokenfile": temp_file_path}
# Act
result = resolve_token(config_auth)
# Assert
assert result == token_content
finally:
os.unlink(temp_file_path)
def test_resolve_token_from_config(self):
"""Test reading token directly from config"""
# Arrange
config_auth = {"token": "s.0000"}
# Act
result = resolve_token(config_auth)
# Assert
assert result == "s.0000"
def test_resolve_token_tokenfile_takes_precedence(self):
"""Test that tokenfile takes precedence over direct token config"""
# Arrange
token_from_file = "s.file-token"
with tempfile.NamedTemporaryFile(mode='w', delete=False, dir='tests') as temp_file:
temp_file.write(token_from_file)
temp_file_path = temp_file.name
try:
config_auth = {
"tokenfile": temp_file_path,
"token": "s.config-token"
}
# Act
result = resolve_token(config_auth)
# Assert
assert result == token_from_file
finally:
os.unlink(temp_file_path)
class TestResolveNamespace:
"""Test the resolve_namespace function"""
def test_resolve_namespace_from_config(self):
"""Test resolving namespace from config"""
# Arrange
config = {"namespace": "quux"}
# Act
result = resolve_namespace(config)
# Assert
assert result == "quux"
def test_resolve_namespace_from_environment(self):
"""Test resolving namespace from environment variable"""
# Arrange
config = {}
# Act & Assert
with patch.dict(os.environ, {"VAULT_NAMESPACE": "quux"}):
result = resolve_namespace(config)
assert result == "quux"
def test_resolve_namespace_config_overrides_environment(self):
"""Test that config value takes precedence over environment variable"""
# Arrange
config = {"namespace": "config-namespace"}
# Act & Assert
with patch.dict(os.environ, {"VAULT_NAMESPACE": "env-namespace"}):
result = resolve_namespace(config)
assert result == "config-namespace"
def test_resolve_namespace_no_config_no_env_returns_none(self):
"""Test that missing config and env var returns None"""
# Arrange
config = {}
# Act & Assert
with patch.dict(os.environ, {}, clear=True):
result = resolve_namespace(config)
assert result is None
class TestLoginWithApprole:
"""Test the login_with_approle function"""
def test_login_with_approle_success(self):
"""Test successful approle login"""
# Arrange
mock_client = Mock()
config_auth = {
"role_id": "test-role-id",
"secret_id": "test-secret-id"
}
# Act
login_with_approle(mock_client, config_auth)
# Assert
mock_client.auth.approle.login.assert_called_once_with(
role_id="test-role-id",
secret_id="test-secret-id"
)
def test_login_with_approle_no_secret_id(self):
"""Test approle login without secret_id (should pass None)"""
# Arrange
mock_client = Mock()
config_auth = {"role_id": "test-role-id"}
# Act
login_with_approle(mock_client, config_auth)
# Assert
mock_client.auth.approle.login.assert_called_once_with(
role_id="test-role-id",
secret_id=None
)
def test_login_with_approle_missing_role_id_raises_error(self):
"""Test that missing role_id raises ValueError"""
# Arrange
mock_client = Mock()
config_auth = {"secret_id": "test-secret-id"}
# Act & Assert
with pytest.raises(ValueError, match="Missing role_id in auth configuration"):
login_with_approle(mock_client, config_auth)
# Additional integration-style tests
class TestIntegration:
"""Integration tests combining multiple functions"""
@patch('secretsmith.vault.client.Client')
def test_full_config_with_all_options(self, mock_client):
"""Test complete configuration with all options"""
# Arrange
config = {
"server": {
"url": "https://vault.example.com",
"verify": "/path/to/ca.crt",
"namespace": "test-namespace"
},
"auth": {
"method": "token",
"token": "s.full-test-token"
}
}
mock_client_instance = Mock()
mock_client.return_value = mock_client_instance
# Act
result = from_config(config)
# Assert
mock_client.assert_called_once_with(
url="https://vault.example.com",
token="s.full-test-token",
verify="/path/to/ca.crt",
namespace="test-namespace"
)
assert result == mock_client_instance
@patch('secretsmith.vault.client.Client')
def test_minimal_config(self, mock_client):
"""Test minimal configuration"""
# Arrange
config = {}
mock_client_instance = Mock()
mock_client.return_value = mock_client_instance
# Act
result = from_config(config)
# Assert
mock_client.assert_called_once_with(
url=None,
token=None,
verify=None,
namespace=None
)
assert result == mock_client_instance