"""Tests for infrastructure endpoints.""" import pytest from fastapi.testclient import TestClient from unittest.mock import patch, AsyncMock, MagicMock from src.main import app @pytest.fixture def client(): """Create a test client.""" return TestClient(app) @pytest.fixture def mock_portainer(): """Create a mock Portainer client.""" mock = AsyncMock() return mock @pytest.fixture def mock_npm(): """Create a mock NPM client.""" mock = AsyncMock() return mock class TestInfrastructureHealth: """Test /infrastructure/health endpoint.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_health_returns_200(self, mock_get_npm, mock_get_portainer, client): """Health endpoint should return 200.""" mock_portainer = AsyncMock() mock_portainer.health_check.return_value = True mock_portainer.get_stacks.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_npm.health_check.return_value = True mock_npm.get_proxy_hosts.return_value = [] mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/health") assert response.status_code == 200 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_health_returns_connection_status(self, mock_get_npm, mock_get_portainer, client): """Health endpoint should return connection status for both services.""" mock_portainer = AsyncMock() mock_portainer.health_check.return_value = True mock_portainer.get_stacks.return_value = [{"Id": 1}, {"Id": 2}] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_npm.health_check.return_value = True mock_npm.get_proxy_hosts.return_value = [{"id": 1}] mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/health") data = response.json() assert "portainer_connected" in data assert "npm_connected" in data assert "total_stacks" in data assert "total_proxy_hosts" in data assert data["portainer_connected"] is True assert data["npm_connected"] is True assert data["total_stacks"] == 2 assert data["total_proxy_hosts"] == 1 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_health_handles_disconnected_services(self, mock_get_npm, mock_get_portainer, client): """Health should handle when services are disconnected.""" mock_portainer = AsyncMock() mock_portainer.health_check.return_value = False mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_npm.health_check.return_value = False mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/health") data = response.json() assert data["portainer_connected"] is False assert data["npm_connected"] is False assert data["total_stacks"] == 0 assert data["total_proxy_hosts"] == 0 class TestInfrastructureServices: """Test /infrastructure/services endpoints.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_list_services_returns_200(self, mock_get_npm, mock_get_portainer, client): """List services should return 200.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_npm.get_proxy_hosts.return_value = [] mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/services") assert response.status_code == 200 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_list_services_returns_stack_info(self, mock_get_npm, mock_get_portainer, client): """List services should return stack information.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "stack1", "Status": 1, "EndpointId": 1}, {"Id": 2, "Name": "stack2", "Status": 2, "EndpointId": 1} ] mock_portainer.get_containers.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_npm.get_proxy_hosts.return_value = [] mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/services") data = response.json() assert isinstance(data, list) assert len(data) == 2 assert data[0]["name"] == "stack1" assert data[1]["name"] == "stack2" class TestInfrastructurePorts: """Test /infrastructure/ports endpoint.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_list_ports_returns_200(self, mock_get_npm, mock_get_portainer, client): """List ports should return 200.""" mock_portainer = AsyncMock() mock_portainer.get_endpoints.return_value = [{"Id": 1}] mock_portainer.get_containers.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_npm.get_proxy_hosts.return_value = [] mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/ports") assert response.status_code == 200 class TestInfrastructureDomains: """Test /infrastructure/domains endpoint.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_list_domains_returns_200(self, mock_get_npm, mock_get_portainer, client): """List domains should return 200.""" mock_portainer = AsyncMock() mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_npm.get_proxy_hosts.return_value = [] mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/domains") assert response.status_code == 200 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_list_domains_returns_domain_info(self, mock_get_npm, mock_get_portainer, client): """List domains should return domain information.""" mock_portainer = AsyncMock() mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_npm.get_proxy_hosts.return_value = [ { "id": 1, "domain_names": ["example.com", "www.example.com"], "forward_host": "app", "forward_port": 8080, "ssl_certificate_id": 1 } ] mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/domains") data = response.json() assert isinstance(data, list) # Each domain name should be a separate entry assert len(data) >= 1 class TestInfrastructureContainers: """Test /infrastructure/containers endpoint.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_list_containers_returns_200(self, mock_get_npm, mock_get_portainer, client): """List containers should return 200.""" mock_portainer = AsyncMock() mock_portainer.list_containers.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/containers") assert response.status_code == 200 class TestInfrastructureWidgetData: """Test /infrastructure/widget-data endpoint.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_widget_data_returns_200(self, mock_get_npm, mock_get_portainer, client): """Widget data should return 200.""" mock_portainer = AsyncMock() mock_portainer.health_check.return_value = True mock_portainer.get_stacks.return_value = [] mock_portainer.list_containers.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_npm.health_check.return_value = True mock_npm.get_proxy_hosts.return_value = [] mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/widget-data") assert response.status_code == 200 class TestInfrastructureServiceGroups: """Test /infrastructure/service-groups endpoint.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_service_groups_returns_200(self, mock_get_npm, mock_get_portainer, client): """Service groups should return 200.""" mock_portainer = AsyncMock() mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/service-groups") assert response.status_code == 200 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_service_groups_returns_group_data(self, mock_get_npm, mock_get_portainer, client): """Service groups should return group data.""" mock_portainer = AsyncMock() mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/service-groups") data = response.json() # Should return a dict or list of service groups assert isinstance(data, (dict, list)) class TestInfrastructureResources: """Test /infrastructure/resources endpoints.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_system_resources_returns_200(self, mock_get_npm, mock_get_portainer, client): """System resources should return 200.""" mock_portainer = AsyncMock() mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/resources/system") assert response.status_code == 200 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_container_resources_returns_200(self, mock_get_npm, mock_get_portainer, client): """Container resources should return 200.""" mock_portainer = AsyncMock() mock_portainer.list_containers.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/resources/containers") assert response.status_code == 200 class TestInfrastructureServiceStatus: """Test /infrastructure/services/{service}/status endpoint.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_service_status_returns_200(self, mock_get_npm, mock_get_portainer, client): """Service status should return 200.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "testservice", "Status": 1, "EndpointId": 1} ] mock_portainer.get_containers.return_value = [ {"Names": ["/testservice_app_1"], "State": "running"} ] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_npm.get_proxy_hosts.return_value = [] mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/services/testservice/status") assert response.status_code == 200 class TestInfrastructureContainerActions: """Test container action endpoints.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_get_container_logs_returns_200(self, mock_get_npm, mock_get_portainer, client): """Get container logs should return 200.""" mock_portainer = AsyncMock() mock_portainer.list_containers.return_value = [ {"Names": ["/testcontainer"], "Id": "abc123"} ] mock_portainer.get_endpoints.return_value = [{"Id": 1}] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/containers/testcontainer/logs") # Response depends on container existence assert response.status_code in [200, 404] @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_get_single_container_returns_200(self, mock_get_npm, mock_get_portainer, client): """Get single container should return 200.""" mock_portainer = AsyncMock() mock_portainer.inspect_container.return_value = { "Id": "abc123", "Name": "/testcontainer", "State": {"Status": "running"} } mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/containers/testcontainer") assert response.status_code == 200 class TestInfrastructureGetService: """Test /infrastructure/services/{name} endpoint.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_get_service_returns_200(self, mock_get_npm, mock_get_portainer, client): """Get service should return 200.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "testservice", "Status": 1, "EndpointId": 1} ] mock_portainer.get_containers.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_npm.get_proxy_hosts.return_value = [] mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/services/testservice") assert response.status_code == 200 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_get_service_returns_404_for_missing(self, mock_get_npm, mock_get_portainer, client): """Get service should return 404 for non-existent service.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/services/nonexistent") assert response.status_code == 404 class TestInfrastructureDeleteContainer: """Test DELETE /infrastructure/containers/{container_id} endpoint.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_delete_container_returns_204(self, mock_get_npm, mock_get_portainer, client): """Delete container should return 204 on success.""" mock_portainer = AsyncMock() mock_portainer.get_endpoints.return_value = [{"Id": 1}] mock_portainer.delete_container.return_value = True mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.delete("/infrastructure/containers/abc123") assert response.status_code == 204 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_delete_container_with_force(self, mock_get_npm, mock_get_portainer, client): """Delete container should pass force parameter.""" mock_portainer = AsyncMock() mock_portainer.get_endpoints.return_value = [{"Id": 1}] mock_portainer.delete_container.return_value = True mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.delete("/infrastructure/containers/abc123?force=true") assert response.status_code == 204 mock_portainer.delete_container.assert_called_once_with(1, "abc123", force=True) @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_delete_container_returns_404_for_missing(self, mock_get_npm, mock_get_portainer, client): """Delete container should return 404 if not found.""" mock_portainer = AsyncMock() mock_portainer.get_endpoints.return_value = [{"Id": 1}] mock_portainer.delete_container.side_effect = Exception("404 no such container") mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.delete("/infrastructure/containers/nonexistent") assert response.status_code == 404 class TestInfrastructureStackCompose: """Test /infrastructure/stacks/{stackId}/compose endpoints.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_get_stack_compose_returns_yaml(self, mock_get_npm, mock_get_portainer, client): """Get stack compose should return YAML content.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "mystack", "EndpointId": 1} ] mock_portainer.get_stack_file.return_value = "version: '3'\nservices:\n web:\n image: nginx" mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/stacks/mystack/compose") assert response.status_code == 200 assert "text/yaml" in response.headers.get("content-type", "") assert "version:" in response.text @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_get_stack_compose_returns_404_for_missing(self, mock_get_npm, mock_get_portainer, client): """Get stack compose should return 404 if stack not found.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/stacks/nonexistent/compose") assert response.status_code == 404 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_put_stack_compose_returns_204(self, mock_get_npm, mock_get_portainer, client): """Update stack compose should return 204 on success.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "mystack", "EndpointId": 1} ] mock_portainer.update_stack.return_value = {"Id": 1} mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.put( "/infrastructure/stacks/mystack/compose", content="version: '3'\nservices:\n web:\n image: nginx:latest", headers={"Content-Type": "text/yaml"} ) assert response.status_code == 204 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_put_stack_compose_returns_400_for_empty(self, mock_get_npm, mock_get_portainer, client): """Update stack compose should return 400 for empty content.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "mystack", "EndpointId": 1} ] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.put( "/infrastructure/stacks/mystack/compose", content="", headers={"Content-Type": "text/yaml"} ) assert response.status_code == 400 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_put_stack_compose_returns_404_for_missing(self, mock_get_npm, mock_get_portainer, client): """Update stack compose should return 404 if stack not found.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.put( "/infrastructure/stacks/nonexistent/compose", content="version: '3'", headers={"Content-Type": "text/yaml"} ) assert response.status_code == 404 class TestInfrastructureStackEnv: """Test /infrastructure/stacks/{stackId}/env endpoints.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_get_stack_env_returns_dict(self, mock_get_npm, mock_get_portainer, client): """Get stack env should return environment variables as dict.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "mystack", "EndpointId": 1} ] mock_portainer.get_stack.return_value = { "Id": 1, "Name": "mystack", "Env": [ {"name": "DB_HOST", "value": "localhost"}, {"name": "DB_PORT", "value": "5432"} ] } mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/stacks/mystack/env") assert response.status_code == 200 data = response.json() assert data == {"DB_HOST": "localhost", "DB_PORT": "5432"} @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_get_stack_env_returns_empty_dict(self, mock_get_npm, mock_get_portainer, client): """Get stack env should return empty dict if no env vars.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "mystack", "EndpointId": 1} ] mock_portainer.get_stack.return_value = {"Id": 1, "Name": "mystack", "Env": []} mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/stacks/mystack/env") assert response.status_code == 200 assert response.json() == {} @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_get_stack_env_returns_404_for_missing(self, mock_get_npm, mock_get_portainer, client): """Get stack env should return 404 if stack not found.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.get("/infrastructure/stacks/nonexistent/env") assert response.status_code == 404 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_put_stack_env_returns_204(self, mock_get_npm, mock_get_portainer, client): """Update stack env should return 204 on success.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "mystack", "EndpointId": 1} ] mock_portainer.update_stack_env.return_value = {"Id": 1} mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.put( "/infrastructure/stacks/mystack/env", json={"DB_HOST": "newhost", "DB_PORT": "5433"} ) assert response.status_code == 204 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_put_stack_env_returns_404_for_missing(self, mock_get_npm, mock_get_portainer, client): """Update stack env should return 404 if stack not found.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.put( "/infrastructure/stacks/nonexistent/env", json={"KEY": "value"} ) assert response.status_code == 404 class TestInfrastructureStackDeploy: """Test /infrastructure/stacks/{stackId}/deploy endpoint.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_deploy_stack_returns_202(self, mock_get_npm, mock_get_portainer, client): """Deploy stack should return 202 Accepted.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "mystack", "EndpointId": 1} ] mock_portainer.redeploy_stack.return_value = {"Id": 1} mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.post("/infrastructure/stacks/mystack/deploy") assert response.status_code == 202 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_deploy_stack_does_not_pull_images(self, mock_get_npm, mock_get_portainer, client): """Deploy stack should not pull images.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "mystack", "EndpointId": 1} ] mock_portainer.redeploy_stack.return_value = {"Id": 1} mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.post("/infrastructure/stacks/mystack/deploy") assert response.status_code == 202 mock_portainer.redeploy_stack.assert_called_once_with( stack_id=1, endpoint_id=1, pull_image=False ) @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_deploy_stack_returns_404_for_missing(self, mock_get_npm, mock_get_portainer, client): """Deploy stack should return 404 if stack not found.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.post("/infrastructure/stacks/nonexistent/deploy") assert response.status_code == 404 class TestInfrastructureStackRebuild: """Test /infrastructure/stacks/{stackId}/rebuild endpoint.""" @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_rebuild_stack_returns_202(self, mock_get_npm, mock_get_portainer, client): """Rebuild stack should return 202 Accepted.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "mystack", "EndpointId": 1} ] mock_portainer.redeploy_stack.return_value = {"Id": 1} mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.post("/infrastructure/stacks/mystack/rebuild") assert response.status_code == 202 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_rebuild_stack_pulls_images(self, mock_get_npm, mock_get_portainer, client): """Rebuild stack should pull images.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "mystack", "EndpointId": 1} ] mock_portainer.redeploy_stack.return_value = {"Id": 1} mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.post("/infrastructure/stacks/mystack/rebuild") assert response.status_code == 202 mock_portainer.redeploy_stack.assert_called_once_with( stack_id=1, endpoint_id=1, pull_image=True ) @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_rebuild_stack_returns_404_for_missing(self, mock_get_npm, mock_get_portainer, client): """Rebuild stack should return 404 if stack not found.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [] mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.post("/infrastructure/stacks/nonexistent/rebuild") assert response.status_code == 404 @patch("src.controllers.infrastructure_controller.get_portainer_client") @patch("src.controllers.infrastructure_controller.get_npm_client") def test_rebuild_stack_case_insensitive(self, mock_get_npm, mock_get_portainer, client): """Rebuild stack should match stack name case-insensitively.""" mock_portainer = AsyncMock() mock_portainer.get_stacks.return_value = [ {"Id": 1, "Name": "MyStack", "EndpointId": 1} ] mock_portainer.redeploy_stack.return_value = {"Id": 1} mock_get_portainer.return_value = mock_portainer mock_npm = AsyncMock() mock_get_npm.return_value = mock_npm response = client.post("/infrastructure/stacks/mystack/rebuild") assert response.status_code == 202