379 lines
13 KiB
Python
379 lines
13 KiB
Python
"""
|
|
Pytest Integration Test Template
|
|
|
|
Complete workflow testing for CLI applications using Click.testing.CliRunner
|
|
Tests multi-command workflows, state persistence, and end-to-end scenarios
|
|
"""
|
|
|
|
import pytest
|
|
import os
|
|
import json
|
|
import yaml
|
|
from pathlib import Path
|
|
from click.testing import CliRunner
|
|
from mycli.cli import cli
|
|
|
|
|
|
@pytest.fixture
|
|
def integration_runner():
|
|
"""Create runner with isolated filesystem for integration tests"""
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
yield runner
|
|
|
|
|
|
class TestDeploymentWorkflow:
|
|
"""Test complete deployment workflow"""
|
|
|
|
def test_full_deployment_workflow(self, integration_runner):
|
|
"""Should complete init -> configure -> build -> deploy workflow"""
|
|
runner = integration_runner
|
|
|
|
# Step 1: Initialize project
|
|
result = runner.invoke(cli, ['init', 'my-project'])
|
|
assert result.exit_code == 0
|
|
assert 'Project initialized' in result.output
|
|
assert os.path.exists('my-project')
|
|
|
|
# Step 2: Configure API key
|
|
os.chdir('my-project')
|
|
result = runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
|
|
assert result.exit_code == 0
|
|
|
|
# Step 3: Build project
|
|
result = runner.invoke(cli, ['build', '--production'])
|
|
assert result.exit_code == 0
|
|
assert 'Build successful' in result.output
|
|
|
|
# Step 4: Deploy to production
|
|
result = runner.invoke(cli, ['deploy', 'production'])
|
|
assert result.exit_code == 0
|
|
assert 'Deployed successfully' in result.output
|
|
|
|
def test_deployment_without_config_fails(self, integration_runner):
|
|
"""Should fail deployment without required configuration"""
|
|
runner = integration_runner
|
|
|
|
# Initialize but don't configure
|
|
runner.invoke(cli, ['init', 'my-project'])
|
|
os.chdir('my-project')
|
|
|
|
# Try to deploy without API key
|
|
result = runner.invoke(cli, ['deploy', 'production'])
|
|
assert result.exit_code != 0
|
|
assert 'api_key' in result.output.lower()
|
|
|
|
def test_deployment_rollback(self, integration_runner):
|
|
"""Should rollback failed deployment"""
|
|
runner = integration_runner
|
|
|
|
# Setup and deploy
|
|
runner.invoke(cli, ['init', 'my-project'])
|
|
os.chdir('my-project')
|
|
runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
|
|
runner.invoke(cli, ['deploy', 'staging'])
|
|
|
|
# Rollback
|
|
result = runner.invoke(cli, ['rollback'])
|
|
assert result.exit_code == 0
|
|
assert 'Rollback successful' in result.output
|
|
|
|
|
|
class TestMultiEnvironmentWorkflow:
|
|
"""Test multi-environment configuration and deployment"""
|
|
|
|
def test_manage_multiple_environments(self, integration_runner):
|
|
"""Should manage dev, staging, and production environments"""
|
|
runner = integration_runner
|
|
|
|
runner.invoke(cli, ['init', 'multi-env-project'])
|
|
os.chdir('multi-env-project')
|
|
|
|
# Configure development
|
|
runner.invoke(cli, ['config', 'set', 'api_key', 'dev_key', '--env', 'development'])
|
|
runner.invoke(cli, ['config', 'set', 'base_url', 'https://dev.api.example.com', '--env', 'development'])
|
|
|
|
# Configure staging
|
|
runner.invoke(cli, ['config', 'set', 'api_key', 'staging_key', '--env', 'staging'])
|
|
runner.invoke(cli, ['config', 'set', 'base_url', 'https://staging.api.example.com', '--env', 'staging'])
|
|
|
|
# Configure production
|
|
runner.invoke(cli, ['config', 'set', 'api_key', 'prod_key', '--env', 'production'])
|
|
runner.invoke(cli, ['config', 'set', 'base_url', 'https://api.example.com', '--env', 'production'])
|
|
|
|
# Deploy to each environment
|
|
dev_result = runner.invoke(cli, ['deploy', 'development'])
|
|
assert dev_result.exit_code == 0
|
|
assert 'dev.api.example.com' in dev_result.output
|
|
|
|
staging_result = runner.invoke(cli, ['deploy', 'staging'])
|
|
assert staging_result.exit_code == 0
|
|
assert 'staging.api.example.com' in staging_result.output
|
|
|
|
prod_result = runner.invoke(cli, ['deploy', 'production'])
|
|
assert prod_result.exit_code == 0
|
|
assert 'api.example.com' in prod_result.output
|
|
|
|
def test_environment_isolation(self, integration_runner):
|
|
"""Should keep environment configurations isolated"""
|
|
runner = integration_runner
|
|
|
|
runner.invoke(cli, ['init', 'isolated-project'])
|
|
os.chdir('isolated-project')
|
|
|
|
# Set different values for each environment
|
|
runner.invoke(cli, ['config', 'set', 'timeout', '10', '--env', 'development'])
|
|
runner.invoke(cli, ['config', 'set', 'timeout', '30', '--env', 'production'])
|
|
|
|
# Verify values are isolated
|
|
dev_result = runner.invoke(cli, ['config', 'get', 'timeout', '--env', 'development'])
|
|
assert '10' in dev_result.output
|
|
|
|
prod_result = runner.invoke(cli, ['config', 'get', 'timeout', '--env', 'production'])
|
|
assert '30' in prod_result.output
|
|
|
|
|
|
class TestStatePersistence:
|
|
"""Test state management and persistence"""
|
|
|
|
def test_state_persistence_across_commands(self, integration_runner):
|
|
"""Should maintain state across multiple commands"""
|
|
runner = integration_runner
|
|
|
|
# Initialize state
|
|
result = runner.invoke(cli, ['state', 'init'])
|
|
assert result.exit_code == 0
|
|
|
|
# Set multiple state values
|
|
runner.invoke(cli, ['state', 'set', 'counter', '0'])
|
|
runner.invoke(cli, ['state', 'set', 'user', 'testuser'])
|
|
|
|
# Increment counter multiple times
|
|
for i in range(5):
|
|
runner.invoke(cli, ['increment'])
|
|
|
|
# Verify final state
|
|
result = runner.invoke(cli, ['state', 'get', 'counter'])
|
|
assert result.exit_code == 0
|
|
assert '5' in result.output
|
|
|
|
result = runner.invoke(cli, ['state', 'get', 'user'])
|
|
assert 'testuser' in result.output
|
|
|
|
def test_state_recovery_from_corruption(self, integration_runner):
|
|
"""Should recover from corrupted state file"""
|
|
runner = integration_runner
|
|
|
|
# Create valid state
|
|
runner.invoke(cli, ['state', 'init'])
|
|
runner.invoke(cli, ['state', 'set', 'key', 'value'])
|
|
|
|
# Corrupt the state file
|
|
with open('.mycli-state', 'w') as f:
|
|
f.write('invalid json {[}')
|
|
|
|
# Should detect corruption and recover
|
|
result = runner.invoke(cli, ['state', 'get', 'key'])
|
|
assert result.exit_code != 0
|
|
assert 'corrupt' in result.output.lower()
|
|
|
|
# Should be able to reset
|
|
result = runner.invoke(cli, ['state', 'reset'])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
class TestPluginWorkflow:
|
|
"""Test plugin installation and usage"""
|
|
|
|
def test_plugin_lifecycle(self, integration_runner):
|
|
"""Should install, use, and uninstall plugins"""
|
|
runner = integration_runner
|
|
|
|
runner.invoke(cli, ['init', 'plugin-project'])
|
|
os.chdir('plugin-project')
|
|
|
|
# Install plugin
|
|
result = runner.invoke(cli, ['plugin', 'install', 'test-plugin'])
|
|
assert result.exit_code == 0
|
|
assert 'installed' in result.output.lower()
|
|
|
|
# Verify plugin is listed
|
|
result = runner.invoke(cli, ['plugin', 'list'])
|
|
assert 'test-plugin' in result.output
|
|
|
|
# Use plugin command
|
|
result = runner.invoke(cli, ['test-plugin:command', '--arg', 'value'])
|
|
assert result.exit_code == 0
|
|
|
|
# Uninstall plugin
|
|
result = runner.invoke(cli, ['plugin', 'uninstall', 'test-plugin'])
|
|
assert result.exit_code == 0
|
|
|
|
# Verify plugin is removed
|
|
result = runner.invoke(cli, ['plugin', 'list'])
|
|
assert 'test-plugin' not in result.output
|
|
|
|
def test_plugin_conflict_detection(self, integration_runner):
|
|
"""Should detect and handle plugin conflicts"""
|
|
runner = integration_runner
|
|
|
|
runner.invoke(cli, ['init', 'conflict-project'])
|
|
os.chdir('conflict-project')
|
|
|
|
# Install first plugin
|
|
runner.invoke(cli, ['plugin', 'install', 'plugin-a'])
|
|
|
|
# Try to install conflicting plugin
|
|
result = runner.invoke(cli, ['plugin', 'install', 'plugin-b'])
|
|
if 'conflict' in result.output.lower():
|
|
assert result.exit_code != 0
|
|
|
|
|
|
class TestDataMigration:
|
|
"""Test data migration workflows"""
|
|
|
|
def test_version_migration(self, integration_runner):
|
|
"""Should migrate data between versions"""
|
|
runner = integration_runner
|
|
|
|
# Create old version data
|
|
old_data = {
|
|
'version': 1,
|
|
'format': 'legacy',
|
|
'data': {'key': 'value'}
|
|
}
|
|
with open('data.json', 'w') as f:
|
|
json.dump(old_data, f)
|
|
|
|
# Run migration
|
|
result = runner.invoke(cli, ['migrate', '--to', '2.0'])
|
|
assert result.exit_code == 0
|
|
|
|
# Verify new format
|
|
with open('data.json', 'r') as f:
|
|
new_data = json.load(f)
|
|
assert new_data['version'] == 2
|
|
assert 'legacy' not in new_data.get('format', '')
|
|
|
|
def test_migration_backup(self, integration_runner):
|
|
"""Should create backup during migration"""
|
|
runner = integration_runner
|
|
|
|
# Create data
|
|
data = {'version': 1, 'data': 'important'}
|
|
with open('data.json', 'w') as f:
|
|
json.dump(data, f)
|
|
|
|
# Migrate with backup
|
|
result = runner.invoke(cli, ['migrate', '--to', '2.0', '--backup'])
|
|
assert result.exit_code == 0
|
|
|
|
# Verify backup exists
|
|
assert os.path.exists('data.json.backup')
|
|
|
|
|
|
class TestConcurrentOperations:
|
|
"""Test handling of concurrent operations"""
|
|
|
|
def test_file_locking(self, integration_runner):
|
|
"""Should prevent concurrent modifications"""
|
|
runner = integration_runner
|
|
|
|
runner.invoke(cli, ['init', 'lock-project'])
|
|
os.chdir('lock-project')
|
|
|
|
# Create lock file
|
|
with open('.mycli.lock', 'w') as f:
|
|
f.write('locked')
|
|
|
|
# Try to run command that needs lock
|
|
result = runner.invoke(cli, ['deploy', 'production'])
|
|
assert result.exit_code != 0
|
|
assert 'lock' in result.output.lower()
|
|
|
|
def test_lock_timeout(self, integration_runner):
|
|
"""Should timeout waiting for lock"""
|
|
runner = integration_runner
|
|
|
|
runner.invoke(cli, ['init', 'timeout-project'])
|
|
os.chdir('timeout-project')
|
|
|
|
# Create stale lock
|
|
with open('.mycli.lock', 'w') as f:
|
|
import time
|
|
f.write(str(time.time() - 3600)) # 1 hour old
|
|
|
|
# Should detect stale lock and continue
|
|
result = runner.invoke(cli, ['build'])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
class TestErrorRecovery:
|
|
"""Test error recovery and retry logic"""
|
|
|
|
def test_retry_on_failure(self, integration_runner):
|
|
"""Should retry failed operations"""
|
|
runner = integration_runner
|
|
|
|
runner.invoke(cli, ['init', 'retry-project'])
|
|
os.chdir('retry-project')
|
|
runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
|
|
|
|
# Simulate failure and retry
|
|
result = runner.invoke(cli, ['deploy', 'staging', '--retry', '3'])
|
|
# Should attempt retry logic
|
|
|
|
def test_partial_failure_recovery(self, integration_runner):
|
|
"""Should recover from partial failures"""
|
|
runner = integration_runner
|
|
|
|
runner.invoke(cli, ['init', 'recovery-project'])
|
|
os.chdir('recovery-project')
|
|
|
|
# Create partial state
|
|
runner.invoke(cli, ['build', '--step', '1'])
|
|
runner.invoke(cli, ['build', '--step', '2'])
|
|
|
|
# Complete from last successful step
|
|
result = runner.invoke(cli, ['build', '--continue'])
|
|
assert result.exit_code == 0
|
|
|
|
|
|
class TestCompleteWorkflow:
|
|
"""Test complete end-to-end workflows"""
|
|
|
|
def test_full_project_lifecycle(self, integration_runner):
|
|
"""Should complete entire project lifecycle"""
|
|
runner = integration_runner
|
|
|
|
# Create project
|
|
result = runner.invoke(cli, ['create', 'full-project'])
|
|
assert result.exit_code == 0
|
|
|
|
os.chdir('full-project')
|
|
|
|
# Configure
|
|
runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
|
|
runner.invoke(cli, ['config', 'set', 'region', 'us-west-1'])
|
|
|
|
# Add dependencies
|
|
result = runner.invoke(cli, ['add', 'dependency', 'package-name'])
|
|
assert result.exit_code == 0
|
|
|
|
# Build
|
|
result = runner.invoke(cli, ['build', '--production'])
|
|
assert result.exit_code == 0
|
|
|
|
# Test
|
|
result = runner.invoke(cli, ['test'])
|
|
assert result.exit_code == 0
|
|
|
|
# Deploy
|
|
result = runner.invoke(cli, ['deploy', 'production'])
|
|
assert result.exit_code == 0
|
|
|
|
# Verify deployment
|
|
result = runner.invoke(cli, ['status'])
|
|
assert result.exit_code == 0
|
|
assert 'deployed' in result.output.lower()
|