"""Tests for dashboard.views.""" import json from unittest.mock import MagicMock, patch from django.core.cache import cache from django.test import RequestFactory, TestCase from dashboard.views import ( api_audits, api_source_status, api_stats, collect_context, index, ) def _minimal_render_context(region_name="test", first_flavor_name="f1", vm_count=1): """Context with all keys the index.html template expects.""" return { "region": {"name": region_name, "hosts_total": 1}, "pcpu": {"total": 1, "usage": 0, "free": 1, "used_percentage": 0}, "vcpu": { "total": 2, "allocated": 1, "free": 1, "allocated_percentage": 50, "overcommit_ratio": 1, "overcommit_max": 2, }, "pram": {"total": 1024**3, "usage": 0, "free": 1024**3, "used_percentage": 0}, "vram": { "total": 1024**3, "allocated": 0, "free": 1024**3, "allocated_percentage": 0, "overcommit_ratio": 0, "overcommit_max": 1, }, "vm": { "count": vm_count, "active": vm_count, "stopped": 0, "avg_cpu": 1, "avg_ram": 0, "density": float(vm_count), }, "flavors": { "first_common_flavor": {"name": first_flavor_name, "count": vm_count}, "second_common_flavor": {"name": "—", "count": 0}, "third_common_flavor": {"name": "—", "count": 0}, }, "audits": [], } class IndexViewTest(TestCase): """Tests for the index view.""" def setUp(self): self.factory = RequestFactory() @patch("dashboard.views.settings") def test_index_use_mock_data_returns_200_and_mock_context(self, mock_settings): mock_settings.USE_MOCK_DATA = True mock_settings.DASHBOARD_CACHE_TTL = 120 request = self.factory.get("/") response = index(request) self.assertEqual(response.status_code, 200) # Mock context contains mock-region and flavors; render uses index.html content = response.content.decode() self.assertIn("mock-region", content) self.assertIn("m1.small", content) @patch("dashboard.views.collect_context") @patch("dashboard.views.settings") def test_index_without_mock_returns_skeleton_and_does_not_call_collect_context( self, mock_settings, mock_collect_context ): mock_settings.USE_MOCK_DATA = False request = self.factory.get("/") response = index(request) self.assertEqual(response.status_code, 200) mock_collect_context.assert_not_called() content = response.content.decode() self.assertIn('data-dashboard="skeleton"', content) self.assertIn("—", content) class CollectContextTest(TestCase): """Tests for collect_context with mocked dependencies.""" def _make_mock_connection(self, region_name="test-region"): conn = MagicMock() conn._compute_region = region_name return conn @patch("dashboard.views.get_current_cluster_cpu") @patch("dashboard.views.fetch_dashboard_metrics") @patch("dashboard.views.get_audits") @patch("dashboard.views.get_flavor_list") @patch("dashboard.views.get_connection") def test_collect_context_structure_and_calculation( self, mock_get_connection, mock_get_flavor_list, mock_get_audits, mock_fetch_metrics, mock_get_current_cluster_cpu, ): mock_get_connection.return_value = self._make_mock_connection("my-region") mock_get_current_cluster_cpu.return_value = { "host_labels": ["h0", "h1"], "cpu_current": [30.0, 40.0], } mock_get_flavor_list.return_value = { "first_common_flavor": {"name": "m1.small", "count": 5}, "second_common_flavor": {"name": "—", "count": 0}, "third_common_flavor": {"name": "—", "count": 0}, } mock_get_audits.return_value = [ { "migrations": [], "host_labels": ["h0", "h1"], "cpu_current": [30.0, 40.0], "cpu_projected": [35.0, 35.0], } ] mock_fetch_metrics.return_value = { "hosts_total": 2, "pcpu_total": 8, "pcpu_usage": 2.5, "vcpu_allocated": 16, "vcpu_overcommit_max": 2.0, "pram_total": 32 * 1024**3, "pram_usage": 8 * 1024**3, "vram_allocated": 24 * 1024**3, "vram_overcommit_max": 1.5, "vm_count": 4, "vm_active": 4, } context = collect_context() self.assertEqual(context["region"]["name"], "my-region") self.assertEqual(context["region"]["hosts_total"], 2) self.assertEqual(context["pcpu"]["total"], 8) self.assertEqual(context["pcpu"]["usage"], 2.5) self.assertEqual(context["vcpu"]["total"], 8 * 2.0) # pcpu_total * vcpu_overcommit_max self.assertEqual(context["vcpu"]["allocated"], 16) self.assertEqual(context["vram"]["total"], 32 * 1024**3 * 1.5) self.assertEqual(context["flavors"]["first_common_flavor"]["name"], "m1.small") self.assertEqual(len(context["audits"]), 1) # Serialized for JS self.assertIsInstance(context["audits"][0]["migrations"], str) self.assertEqual(json.loads(context["audits"][0]["host_labels"]), ["h0", "h1"]) self.assertIn("current_cluster", context) self.assertEqual(json.loads(context["current_cluster"]["host_labels"]), ["h0", "h1"]) self.assertEqual(json.loads(context["current_cluster"]["cpu_current"]), [30.0, 40.0]) class ApiStatsTest(TestCase): """Tests for api_stats view.""" def setUp(self): self.factory = RequestFactory() @patch("dashboard.views.fetch_dashboard_metrics") @patch("dashboard.views.get_flavor_list") @patch("dashboard.views.get_connection") def test_api_stats_returns_json_with_expected_keys( self, mock_get_connection, mock_get_flavor_list, mock_fetch_metrics ): conn = MagicMock() conn._compute_region = "api-region" mock_get_connection.return_value = conn mock_get_flavor_list.return_value = { "first_common_flavor": {"name": "m1.small", "count": 3}, "second_common_flavor": {"name": "—", "count": 0}, "third_common_flavor": {"name": "—", "count": 0}, } mock_fetch_metrics.return_value = { "hosts_total": 2, "pcpu_total": 4, "pcpu_usage": 1.0, "vcpu_allocated": 8, "vcpu_overcommit_max": 2.0, "pram_total": 16 * 1024**3, "pram_usage": 4 * 1024**3, "vram_allocated": 12 * 1024**3, "vram_overcommit_max": 1.5, "vm_count": 2, "vm_active": 2, } cache.clear() request = self.factory.get("/api/stats/") with patch("dashboard.views.settings") as mock_settings: mock_settings.DASHBOARD_CACHE_TTL = 120 response = api_stats(request) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "application/json") data = json.loads(response.content) self.assertEqual(data["region"]["name"], "api-region") self.assertEqual(data["region"]["hosts_total"], 2) self.assertIn("pcpu", data) self.assertIn("pram", data) self.assertIn("vcpu", data) self.assertIn("vram", data) self.assertIn("vm", data) self.assertIn("flavors", data) self.assertEqual(data["flavors"]["first_common_flavor"]["name"], "m1.small") @patch("dashboard.views.collect_stats") @patch("dashboard.views.settings") def test_api_stats_uses_cache(self, mock_settings, mock_collect_stats): mock_settings.DASHBOARD_CACHE_TTL = 120 cached = { "region": {"name": "cached", "hosts_total": 1}, "pcpu": {}, "pram": {}, "vcpu": {}, "vram": {}, "vm": {}, "flavors": {}, } cache.clear() cache.set("dashboard_stats", cached, timeout=120) request = self.factory.get("/api/stats/") response = api_stats(request) mock_collect_stats.assert_not_called() self.assertEqual(json.loads(response.content)["region"]["name"], "cached") class ApiAuditsTest(TestCase): """Tests for api_audits view.""" def setUp(self): self.factory = RequestFactory() @patch("dashboard.views.get_current_cluster_cpu") @patch("dashboard.views.get_audits") @patch("dashboard.views.get_connection") def test_api_audits_returns_json_audits_list( self, mock_get_connection, mock_get_audits, mock_get_current_cluster_cpu ): mock_get_connection.return_value = MagicMock() mock_get_audits.return_value = [ { "id": "audit-1", "name": "Test Audit", "created_at": "2025-02-01T10:00:00", "strategy": "Balanced", "goal": "BALANCED", "scope": "Full Cluster", "cpu_weight": "1.0", "ram_weight": "1.0", "migrations": [ { "instanceName": "i1", "source": "h0", "destination": "h1", "flavor": "m1.small", "impact": "Low", } ], "host_labels": ["h0", "h1"], "cpu_current": [30.0, 40.0], "cpu_projected": [35.0, 35.0], } ] mock_get_current_cluster_cpu.return_value = { "host_labels": ["h0", "h1"], "cpu_current": [30.0, 40.0], } cache.clear() request = self.factory.get("/api/audits/") with patch("dashboard.views.settings") as mock_settings: mock_settings.DASHBOARD_CACHE_TTL = 120 response = api_audits(request) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "application/json") data = json.loads(response.content) self.assertIn("audits", data) self.assertEqual(len(data["audits"]), 1) self.assertEqual(data["audits"][0]["name"], "Test Audit") self.assertIsInstance(data["audits"][0]["migrations"], str) self.assertIsInstance(data["audits"][0]["host_labels"], str) self.assertIn("current_cluster", data) self.assertEqual(data["current_cluster"]["host_labels"], ["h0", "h1"]) self.assertEqual(data["current_cluster"]["cpu_current"], [30.0, 40.0]) @patch("dashboard.views.get_current_cluster_cpu") @patch("dashboard.views.collect_audits") @patch("dashboard.views.settings") def test_api_audits_uses_cache( self, mock_settings, mock_collect_audits, mock_get_current_cluster_cpu ): mock_settings.DASHBOARD_CACHE_TTL = 120 cached_audits = [ { "id": "cached-1", "name": "Cached Audit", "migrations": "[]", "host_labels": "[]", "cpu_current": "[]", "cpu_projected": "[]", } ] cached_cluster = {"host_labels": ["cached-h0"], "cpu_current": [10.0]} cache.clear() cache.set("dashboard_audits", cached_audits, timeout=120) cache.set("dashboard_current_cluster", cached_cluster, timeout=120) request = self.factory.get("/api/audits/") response = api_audits(request) mock_collect_audits.assert_not_called() mock_get_current_cluster_cpu.assert_not_called() data = json.loads(response.content) self.assertEqual(data["audits"][0]["name"], "Cached Audit") self.assertEqual(data["current_cluster"], cached_cluster) class ApiSourceStatusTest(TestCase): """Tests for api_source_status view.""" def setUp(self): self.factory = RequestFactory() @patch("dashboard.views.settings") def test_api_source_status_mock_returns_mock_status(self, mock_settings): mock_settings.USE_MOCK_DATA = True mock_settings.SOURCE_STATUS_CACHE_TTL = 30 request = self.factory.get("/api/source-status/") response = api_source_status(request) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "application/json") data = json.loads(response.content) self.assertEqual(data["prometheus"]["status"], "mock") self.assertEqual(data["openstack"]["status"], "mock") @patch("dashboard.views.check_openstack") @patch("dashboard.views.check_prometheus") @patch("dashboard.views.settings") def test_api_source_status_both_ok_returns_ok( self, mock_settings, mock_check_prometheus, mock_check_openstack ): mock_settings.USE_MOCK_DATA = False mock_settings.SOURCE_STATUS_CACHE_TTL = 30 mock_check_prometheus.return_value = {"status": "ok"} mock_check_openstack.return_value = {"status": "ok"} cache.clear() request = self.factory.get("/api/source-status/") response = api_source_status(request) self.assertEqual(response.status_code, 200) data = json.loads(response.content) self.assertEqual(data["prometheus"]["status"], "ok") self.assertEqual(data["openstack"]["status"], "ok") mock_check_prometheus.assert_called_once() mock_check_openstack.assert_called_once() @patch("dashboard.views.check_openstack") @patch("dashboard.views.check_prometheus") @patch("dashboard.views.settings") def test_api_source_status_prometheus_error_returns_error_message( self, mock_settings, mock_check_prometheus, mock_check_openstack ): mock_settings.USE_MOCK_DATA = False mock_settings.SOURCE_STATUS_CACHE_TTL = 30 mock_check_prometheus.return_value = {"status": "error", "message": "Connection refused"} mock_check_openstack.return_value = {"status": "ok"} cache.clear() request = self.factory.get("/api/source-status/") response = api_source_status(request) self.assertEqual(response.status_code, 200) data = json.loads(response.content) self.assertEqual(data["prometheus"]["status"], "error") self.assertEqual(data["prometheus"]["message"], "Connection refused") self.assertEqual(data["openstack"]["status"], "ok") @patch("dashboard.views.check_openstack") @patch("dashboard.views.check_prometheus") @patch("dashboard.views.settings") def test_api_source_status_openstack_error_returns_error_message( self, mock_settings, mock_check_prometheus, mock_check_openstack ): mock_settings.USE_MOCK_DATA = False mock_settings.SOURCE_STATUS_CACHE_TTL = 30 mock_check_prometheus.return_value = {"status": "ok"} mock_check_openstack.return_value = {"status": "error", "message": "Auth failed"} cache.clear() request = self.factory.get("/api/source-status/") response = api_source_status(request) self.assertEqual(response.status_code, 200) data = json.loads(response.content) self.assertEqual(data["prometheus"]["status"], "ok") self.assertEqual(data["openstack"]["status"], "error") self.assertEqual(data["openstack"]["message"], "Auth failed") @patch("dashboard.views.check_openstack") @patch("dashboard.views.check_prometheus") @patch("dashboard.views.settings") def test_api_source_status_uses_cache( self, mock_settings, mock_check_prometheus, mock_check_openstack ): mock_settings.USE_MOCK_DATA = False mock_settings.SOURCE_STATUS_CACHE_TTL = 30 cache.clear() cached = { "prometheus": {"status": "ok"}, "openstack": {"status": "ok"}, } cache.set("dashboard_source_status", cached, timeout=30) request = self.factory.get("/api/source-status/") response = api_source_status(request) mock_check_prometheus.assert_not_called() mock_check_openstack.assert_not_called() data = json.loads(response.content) self.assertEqual(data["prometheus"]["status"], "ok") self.assertEqual(data["openstack"]["status"], "ok")