""" Unit tests for deployment CLI commands """ import pytest from click.testing import CliRunner from unittest.mock import Mock, patch, MagicMock from src.cli.commands import app from src.database.models import User, SiteDeployment from src.deployment.bunnynet import ( StorageZoneResult, PullZoneResult, BunnyNetAuthError, BunnyNetResourceConflictError, BunnyNetAPIError ) @pytest.fixture def cli_runner(): """Fixture to create a Click CLI test runner""" return CliRunner() @pytest.fixture def mock_admin_user(): """Fixture for a mock admin user""" user = Mock(spec=User) user.id = 1 user.username = "admin" user.role = "Admin" user.is_admin.return_value = True return user @pytest.fixture def mock_site_deployment(): """Fixture for a mock site deployment""" site = Mock(spec=SiteDeployment) site.id = 1 site.site_name = "Test Site" site.custom_hostname = "www.example.com" site.storage_zone_id = 12345 site.storage_zone_name = "test-storage" site.storage_zone_password = "test-pass" site.storage_zone_region = "DE" site.pull_zone_id = 67890 site.pull_zone_bcdn_hostname = "test-cdn.b-cdn.net" site.created_at = Mock() site.created_at.strftime.return_value = "2024-01-01 00:00:00" site.updated_at = Mock() site.updated_at.strftime.return_value = "2024-01-01 00:00:00" return site class TestProvisionSiteCommand: """Tests for provision-site CLI command""" @patch('src.cli.commands.authenticate_admin') @patch('src.cli.commands.get_bunny_account_api_key') @patch('src.cli.commands.BunnyNetClient') @patch('src.cli.commands.db_manager') def test_provision_site_success( self, mock_db, mock_client_class, mock_get_key, mock_auth, cli_runner, mock_admin_user ): """Test successful site provisioning""" mock_auth.return_value = mock_admin_user mock_get_key.return_value = "test_api_key" mock_client = Mock() mock_client_class.return_value = mock_client mock_client.create_storage_zone.return_value = StorageZoneResult( id=12345, name="test-storage", password="test-pass", region="DE" ) mock_client.create_pull_zone.return_value = PullZoneResult( id=67890, name="test-cdn", hostname="test-cdn.b-cdn.net" ) mock_client.add_custom_hostname.return_value = True mock_session = Mock() mock_db.get_session.return_value = mock_session mock_repo = Mock() mock_repo.exists.return_value = False mock_repo.create.return_value = Mock() with patch('src.cli.commands.SiteDeploymentRepository', return_value=mock_repo): result = cli_runner.invoke(app, [ 'provision-site', '--name', 'Test Site', '--domain', 'www.example.com', '--storage-name', 'test-storage', '--region', 'DE', '--admin-user', 'admin', '--admin-password', 'password' ]) assert result.exit_code == 0 assert "Site provisioned successfully!" in result.output assert "MANUAL DNS CONFIGURATION REQUIRED" in result.output mock_client.create_storage_zone.assert_called_once_with("test-storage", "DE") mock_client.create_pull_zone.assert_called_once() mock_client.add_custom_hostname.assert_called_once() @patch('src.cli.commands.authenticate_admin') def test_provision_site_auth_failure(self, mock_auth, cli_runner): """Test provision-site with authentication failure""" mock_auth.return_value = None result = cli_runner.invoke(app, [ 'provision-site', '--name', 'Test Site', '--domain', 'www.example.com', '--storage-name', 'test-storage', '--region', 'DE', '--admin-user', 'admin', '--admin-password', 'wrong' ]) assert result.exit_code != 0 assert "Authentication failed" in result.output @patch('src.cli.commands.authenticate_admin') @patch('src.cli.commands.get_bunny_account_api_key') def test_provision_site_missing_api_key(self, mock_get_key, mock_auth, cli_runner, mock_admin_user): """Test provision-site with missing API key""" mock_auth.return_value = mock_admin_user mock_get_key.side_effect = ValueError("BUNNY_ACCOUNT_API_KEY environment variable is required") result = cli_runner.invoke(app, [ 'provision-site', '--name', 'Test Site', '--domain', 'www.example.com', '--storage-name', 'test-storage', '--region', 'DE', '--admin-user', 'admin', '--admin-password', 'password' ]) assert result.exit_code != 0 assert "BUNNY_ACCOUNT_API_KEY" in result.output @patch('src.cli.commands.authenticate_admin') @patch('src.cli.commands.get_bunny_account_api_key') @patch('src.cli.commands.BunnyNetClient') @patch('src.cli.commands.db_manager') def test_provision_site_domain_exists( self, mock_db, mock_client_class, mock_get_key, mock_auth, cli_runner, mock_admin_user ): """Test provision-site with existing domain""" mock_auth.return_value = mock_admin_user mock_get_key.return_value = "test_api_key" mock_session = Mock() mock_db.get_session.return_value = mock_session mock_repo = Mock() mock_repo.exists.return_value = True with patch('src.cli.commands.SiteDeploymentRepository', return_value=mock_repo): result = cli_runner.invoke(app, [ 'provision-site', '--name', 'Test Site', '--domain', 'www.example.com', '--storage-name', 'test-storage', '--region', 'DE', '--admin-user', 'admin', '--admin-password', 'password' ]) assert result.exit_code != 0 assert "already exists" in result.output class TestAttachDomainCommand: """Tests for attach-domain CLI command""" @patch('src.cli.commands.authenticate_admin') @patch('src.cli.commands.get_bunny_account_api_key') @patch('src.cli.commands.BunnyNetClient') @patch('src.cli.commands.db_manager') def test_attach_domain_success( self, mock_db, mock_client_class, mock_get_key, mock_auth, cli_runner, mock_admin_user ): """Test successful domain attachment""" mock_auth.return_value = mock_admin_user mock_get_key.return_value = "test_api_key" mock_client = Mock() mock_client_class.return_value = mock_client mock_client.find_storage_zone_by_name.return_value = StorageZoneResult( id=12345, name="existing-storage", password="test-pass", region="DE" ) mock_client.create_pull_zone.return_value = PullZoneResult( id=67890, name="test-cdn", hostname="test-cdn.b-cdn.net" ) mock_client.add_custom_hostname.return_value = True mock_session = Mock() mock_db.get_session.return_value = mock_session mock_repo = Mock() mock_repo.exists.return_value = False mock_repo.create.return_value = Mock() with patch('src.cli.commands.SiteDeploymentRepository', return_value=mock_repo): result = cli_runner.invoke(app, [ 'attach-domain', '--name', 'Test Site', '--domain', 'www.example.com', '--storage-name', 'existing-storage', '--admin-user', 'admin', '--admin-password', 'password' ]) assert result.exit_code == 0 assert "Domain attached successfully!" in result.output assert "MANUAL DNS CONFIGURATION REQUIRED" in result.output mock_client.find_storage_zone_by_name.assert_called_once_with("existing-storage") @patch('src.cli.commands.authenticate_admin') @patch('src.cli.commands.get_bunny_account_api_key') @patch('src.cli.commands.BunnyNetClient') @patch('src.cli.commands.db_manager') def test_attach_domain_storage_not_found( self, mock_db, mock_client_class, mock_get_key, mock_auth, cli_runner, mock_admin_user ): """Test attach-domain with non-existent storage zone""" mock_auth.return_value = mock_admin_user mock_get_key.return_value = "test_api_key" mock_client = Mock() mock_client_class.return_value = mock_client mock_client.find_storage_zone_by_name.return_value = None mock_session = Mock() mock_db.get_session.return_value = mock_session mock_repo = Mock() mock_repo.exists.return_value = False with patch('src.cli.commands.SiteDeploymentRepository', return_value=mock_repo): result = cli_runner.invoke(app, [ 'attach-domain', '--name', 'Test Site', '--domain', 'www.example.com', '--storage-name', 'nonexistent', '--admin-user', 'admin', '--admin-password', 'password' ]) assert result.exit_code != 0 assert "not found" in result.output class TestListSitesCommand: """Tests for list-sites CLI command""" @patch('src.cli.commands.authenticate_admin') @patch('src.cli.commands.db_manager') def test_list_sites_success(self, mock_db, mock_auth, cli_runner, mock_admin_user, mock_site_deployment): """Test successful sites listing""" mock_auth.return_value = mock_admin_user mock_session = Mock() mock_db.get_session.return_value = mock_session mock_repo = Mock() mock_repo.get_all.return_value = [mock_site_deployment] with patch('src.cli.commands.SiteDeploymentRepository', return_value=mock_repo): result = cli_runner.invoke(app, [ 'list-sites', '--admin-user', 'admin', '--admin-password', 'password' ]) assert result.exit_code == 0 assert "Total sites: 1" in result.output assert "www.example.com" in result.output @patch('src.cli.commands.authenticate_admin') @patch('src.cli.commands.db_manager') def test_list_sites_empty(self, mock_db, mock_auth, cli_runner, mock_admin_user): """Test listing sites when none exist""" mock_auth.return_value = mock_admin_user mock_session = Mock() mock_db.get_session.return_value = mock_session mock_repo = Mock() mock_repo.get_all.return_value = [] with patch('src.cli.commands.SiteDeploymentRepository', return_value=mock_repo): result = cli_runner.invoke(app, [ 'list-sites', '--admin-user', 'admin', '--admin-password', 'password' ]) assert result.exit_code == 0 assert "No site deployments found" in result.output class TestGetSiteCommand: """Tests for get-site CLI command""" @patch('src.cli.commands.authenticate_admin') @patch('src.cli.commands.db_manager') def test_get_site_success(self, mock_db, mock_auth, cli_runner, mock_admin_user, mock_site_deployment): """Test successful site details retrieval""" mock_auth.return_value = mock_admin_user mock_session = Mock() mock_db.get_session.return_value = mock_session mock_repo = Mock() mock_repo.get_by_hostname.return_value = mock_site_deployment with patch('src.cli.commands.SiteDeploymentRepository', return_value=mock_repo): result = cli_runner.invoke(app, [ 'get-site', '--domain', 'www.example.com', '--admin-user', 'admin', '--admin-password', 'password' ]) assert result.exit_code == 0 assert "Site Deployment Details" in result.output assert "www.example.com" in result.output assert "test-storage" in result.output @patch('src.cli.commands.authenticate_admin') @patch('src.cli.commands.db_manager') def test_get_site_not_found(self, mock_db, mock_auth, cli_runner, mock_admin_user): """Test get-site with non-existent domain""" mock_auth.return_value = mock_admin_user mock_session = Mock() mock_db.get_session.return_value = mock_session mock_repo = Mock() mock_repo.get_by_hostname.return_value = None with patch('src.cli.commands.SiteDeploymentRepository', return_value=mock_repo): result = cli_runner.invoke(app, [ 'get-site', '--domain', 'nonexistent.com', '--admin-user', 'admin', '--admin-password', 'password' ]) assert result.exit_code != 0 assert "not found" in result.output class TestRemoveSiteCommand: """Tests for remove-site CLI command""" @patch('src.cli.commands.authenticate_admin') @patch('src.cli.commands.db_manager') def test_remove_site_success(self, mock_db, mock_auth, cli_runner, mock_admin_user, mock_site_deployment): """Test successful site removal""" mock_auth.return_value = mock_admin_user mock_session = Mock() mock_db.get_session.return_value = mock_session mock_repo = Mock() mock_repo.get_by_hostname.return_value = mock_site_deployment mock_repo.delete.return_value = True with patch('src.cli.commands.SiteDeploymentRepository', return_value=mock_repo): result = cli_runner.invoke(app, [ 'remove-site', '--domain', 'www.example.com', '--admin-user', 'admin', '--admin-password', 'password', '--yes' ]) assert result.exit_code == 0 assert "has been removed" in result.output assert "does NOT delete resources from bunny.net" in result.output @patch('src.cli.commands.authenticate_admin') @patch('src.cli.commands.db_manager') def test_remove_site_not_found(self, mock_db, mock_auth, cli_runner, mock_admin_user): """Test remove-site with non-existent domain""" mock_auth.return_value = mock_admin_user mock_session = Mock() mock_db.get_session.return_value = mock_session mock_repo = Mock() mock_repo.get_by_hostname.return_value = None with patch('src.cli.commands.SiteDeploymentRepository', return_value=mock_repo): result = cli_runner.invoke(app, [ 'remove-site', '--domain', 'nonexistent.com', '--admin-user', 'admin', '--admin-password', 'password', '--yes' ]) assert result.exit_code != 0 assert "not found" in result.output