Add comprehensive E2E tests for materia CLI
- Add pytest and pytest-cov for testing - Add niquests for modern HTTP/2 support (keep requests for hcloud compatibility) - Create 13 E2E tests covering CLI, workers, pipelines, and secrets (71% coverage) - Fix Pulumi ESC environment path (beanflows/prod) and secret key names - Update GitLab CI to run CLI tests with coverage reporting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
99
tests/conftest.py
Normal file
99
tests/conftest.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Pytest configuration and fixtures."""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_esc_env(tmp_path):
|
||||
"""Mock Pulumi ESC environment variables."""
|
||||
ssh_key_path = tmp_path / "test_key"
|
||||
ssh_key_path.write_text("-----BEGIN OPENSSH PRIVATE KEY-----\ntest\n-----END OPENSSH PRIVATE KEY-----")
|
||||
|
||||
return {
|
||||
"HETZNER_API_TOKEN": "test-hetzner-token",
|
||||
"R2_ACCESS_KEY_ID": "test-r2-key",
|
||||
"R2_SECRET_ACCESS_KEY": "test-r2-secret",
|
||||
"R2_ENDPOINT": "test.r2.cloudflarestorage.com",
|
||||
"R2_ARTIFACTS_BUCKET": "test-artifacts",
|
||||
"SSH_PUBLIC_KEY": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITest",
|
||||
"SSH_PRIVATE_KEY": "-----BEGIN OPENSSH PRIVATE KEY-----\ntest\n-----END OPENSSH PRIVATE KEY-----",
|
||||
"SSH_PRIVATE_KEY_PATH": str(ssh_key_path),
|
||||
"CLOUDFLARE_API_TOKEN": "test-cf-token",
|
||||
"ICEBERG_REST_URI": "https://api.cloudflare.com/test",
|
||||
"R2_WAREHOUSE_NAME": "test-warehouse",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_secrets(mock_esc_env):
|
||||
"""Mock the secrets module to return test secrets."""
|
||||
with patch("materia.secrets._load_environment", return_value=mock_esc_env):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_hcloud_client():
|
||||
"""Mock Hetzner Cloud client."""
|
||||
with patch("materia.providers.hetzner.Client") as mock_client:
|
||||
client_instance = Mock()
|
||||
mock_client.return_value = client_instance
|
||||
|
||||
client_instance.ssh_keys.get_all.return_value = []
|
||||
client_instance.ssh_keys.create.return_value = Mock(id=1, name="materia-key")
|
||||
|
||||
mock_server = Mock()
|
||||
mock_server.id = 12345
|
||||
mock_server.name = "test-worker"
|
||||
mock_server.status = "running"
|
||||
mock_server.public_net.ipv4.ip = "192.0.2.1"
|
||||
mock_server.server_type.name = "ccx12"
|
||||
mock_server.wait_until_status_is = Mock()
|
||||
mock_server.delete = Mock()
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.server = mock_server
|
||||
|
||||
client_instance.servers.create.return_value = mock_response
|
||||
client_instance.servers.get_all.return_value = []
|
||||
client_instance.servers.get_by_id.return_value = mock_server
|
||||
|
||||
yield client_instance
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ssh_wait():
|
||||
"""Mock SSH wait function to return immediately."""
|
||||
with patch("materia.providers.hetzner.wait_for_ssh", return_value=True):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ssh_connection():
|
||||
"""Mock paramiko SSH connection."""
|
||||
with patch("materia.pipelines.paramiko.SSHClient") as mock_ssh_class, \
|
||||
patch("materia.pipelines.paramiko.RSAKey.from_private_key_file") as mock_key:
|
||||
ssh_instance = Mock()
|
||||
mock_ssh_class.return_value = ssh_instance
|
||||
mock_key.return_value = Mock()
|
||||
|
||||
ssh_instance.connect = Mock()
|
||||
ssh_instance.set_missing_host_key_policy = Mock()
|
||||
|
||||
mock_channel = Mock()
|
||||
mock_channel.recv_exit_status.return_value = 0
|
||||
|
||||
mock_stdout = Mock()
|
||||
mock_stdout.read.return_value = b"Success\n"
|
||||
mock_stdout.channel = mock_channel
|
||||
|
||||
mock_stderr = Mock()
|
||||
mock_stderr.read.return_value = b""
|
||||
|
||||
ssh_instance.exec_command = Mock(
|
||||
return_value=(Mock(), mock_stdout, mock_stderr)
|
||||
)
|
||||
ssh_instance.close = Mock()
|
||||
|
||||
yield ssh_instance
|
||||
174
tests/test_cli_e2e.py
Normal file
174
tests/test_cli_e2e.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""End-to-end tests for the materia CLI."""
|
||||
|
||||
from typer.testing import CliRunner
|
||||
from materia.cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_cli_help():
|
||||
"""Test that the CLI shows help."""
|
||||
result = runner.invoke(app, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "BeanFlows.coffee data platform management CLI" in result.stdout
|
||||
|
||||
|
||||
def test_cli_version():
|
||||
"""Test version command."""
|
||||
result = runner.invoke(app, ["version"])
|
||||
assert result.exit_code == 0
|
||||
assert "Materia CLI" in result.stdout
|
||||
|
||||
|
||||
def test_secrets_test_command(mock_secrets):
|
||||
"""Test secrets test command."""
|
||||
result = runner.invoke(app, ["secrets", "test"])
|
||||
assert result.exit_code == 0
|
||||
assert "ESC connection successful" in result.stdout
|
||||
|
||||
|
||||
def test_secrets_list_command(mock_secrets):
|
||||
"""Test secrets list command."""
|
||||
result = runner.invoke(app, ["secrets", "list"])
|
||||
assert result.exit_code == 0
|
||||
assert "HETZNER_API_TOKEN" in result.stdout
|
||||
assert "R2_ACCESS_KEY_ID" in result.stdout
|
||||
|
||||
|
||||
def test_worker_list_empty(mock_secrets, mock_hcloud_client):
|
||||
"""Test worker list with no active workers."""
|
||||
result = runner.invoke(app, ["worker", "list"])
|
||||
assert result.exit_code == 0
|
||||
assert "No active workers" in result.stdout
|
||||
|
||||
|
||||
def test_worker_list_with_workers(mock_secrets, mock_hcloud_client):
|
||||
"""Test worker list with active workers."""
|
||||
mock_server = mock_hcloud_client.servers.get_all.return_value[0:1]
|
||||
mock_server = [
|
||||
type(
|
||||
"Server",
|
||||
(),
|
||||
{
|
||||
"id": 12345,
|
||||
"name": "test-worker",
|
||||
"status": "running",
|
||||
"public_net": type("Net", (), {"ipv4": type("IP", (), {"ip": "192.0.2.1"})()})(),
|
||||
"server_type": type("Type", (), {"name": "ccx12"})(),
|
||||
},
|
||||
)()
|
||||
]
|
||||
mock_hcloud_client.servers.get_all.return_value = mock_server
|
||||
|
||||
result = runner.invoke(app, ["worker", "list"])
|
||||
assert result.exit_code == 0
|
||||
assert "test-worker" in result.stdout
|
||||
assert "192.0.2.1" in result.stdout
|
||||
|
||||
|
||||
def test_worker_create(mock_secrets, mock_hcloud_client, mock_ssh_wait):
|
||||
"""Test worker creation."""
|
||||
result = runner.invoke(app, ["worker", "create", "test-worker", "--type", "ccx12"])
|
||||
assert result.exit_code == 0
|
||||
assert "Worker created" in result.stdout
|
||||
assert "192.0.2.1" in result.stdout
|
||||
|
||||
mock_hcloud_client.servers.create.assert_called_once()
|
||||
|
||||
|
||||
def test_worker_destroy(mock_secrets, mock_hcloud_client):
|
||||
"""Test worker destruction."""
|
||||
mock_server = type(
|
||||
"Server",
|
||||
(),
|
||||
{
|
||||
"id": 12345,
|
||||
"name": "test-worker",
|
||||
"status": "running",
|
||||
"public_net": type("Net", (), {"ipv4": type("IP", (), {"ip": "192.0.2.1"})()})(),
|
||||
"server_type": type("Type", (), {"name": "ccx12"})(),
|
||||
"delete": lambda: None,
|
||||
},
|
||||
)()
|
||||
mock_hcloud_client.servers.get_all.return_value = [mock_server]
|
||||
|
||||
result = runner.invoke(app, ["worker", "destroy", "test-worker", "--force"])
|
||||
assert result.exit_code == 0
|
||||
assert "Worker destroyed" in result.stdout
|
||||
|
||||
|
||||
def test_pipeline_list(mock_secrets):
|
||||
"""Test pipeline list command."""
|
||||
result = runner.invoke(app, ["pipeline", "list"])
|
||||
assert result.exit_code == 0
|
||||
assert "extract" in result.stdout
|
||||
assert "transform" in result.stdout
|
||||
assert "ccx12" in result.stdout
|
||||
assert "ccx22" in result.stdout
|
||||
|
||||
|
||||
def test_pipeline_run_extract(
|
||||
mock_secrets, mock_hcloud_client, mock_ssh_wait, mock_ssh_connection
|
||||
):
|
||||
"""Test running extract pipeline end-to-end."""
|
||||
result = runner.invoke(app, ["pipeline", "run", "extract"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Running pipeline" in result.stdout
|
||||
assert "Pipeline completed successfully" in result.stdout
|
||||
|
||||
mock_hcloud_client.servers.create.assert_called_once()
|
||||
mock_ssh_connection.connect.assert_called()
|
||||
mock_ssh_connection.exec_command.assert_called()
|
||||
|
||||
|
||||
def test_pipeline_run_transform(
|
||||
mock_secrets, mock_hcloud_client, mock_ssh_wait, mock_ssh_connection
|
||||
):
|
||||
"""Test running transform pipeline end-to-end."""
|
||||
result = runner.invoke(app, ["pipeline", "run", "transform"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Running pipeline" in result.stdout
|
||||
assert "Pipeline completed successfully" in result.stdout
|
||||
|
||||
mock_hcloud_client.servers.create.assert_called_once()
|
||||
mock_ssh_connection.connect.assert_called()
|
||||
|
||||
|
||||
def test_pipeline_run_invalid(mock_secrets):
|
||||
"""Test running an invalid pipeline."""
|
||||
result = runner.invoke(app, ["pipeline", "run", "invalid-pipeline"])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "Unknown pipeline" in result.stdout or "Unknown pipeline" in result.stderr
|
||||
|
||||
|
||||
def test_worker_lifecycle_e2e(mock_secrets, mock_hcloud_client, mock_ssh_wait):
|
||||
"""Test complete worker lifecycle: create -> list -> destroy."""
|
||||
create_result = runner.invoke(
|
||||
app, ["worker", "create", "lifecycle-test", "--type", "ccx12"]
|
||||
)
|
||||
assert create_result.exit_code == 0
|
||||
assert "Worker created" in create_result.stdout
|
||||
|
||||
mock_server = type(
|
||||
"Server",
|
||||
(),
|
||||
{
|
||||
"id": 12345,
|
||||
"name": "lifecycle-test",
|
||||
"status": "running",
|
||||
"public_net": type("Net", (), {"ipv4": type("IP", (), {"ip": "192.0.2.1"})()})(),
|
||||
"server_type": type("Type", (), {"name": "ccx12"})(),
|
||||
},
|
||||
)()
|
||||
mock_hcloud_client.servers.get_all.return_value = [mock_server]
|
||||
|
||||
list_result = runner.invoke(app, ["worker", "list"])
|
||||
assert list_result.exit_code == 0
|
||||
assert "lifecycle-test" in list_result.stdout
|
||||
|
||||
destroy_result = runner.invoke(app, ["worker", "destroy", "lifecycle-test", "--force"])
|
||||
assert destroy_result.exit_code == 0
|
||||
assert "Worker destroyed" in destroy_result.stdout
|
||||
Reference in New Issue
Block a user