1# Copyright 2012 United States Government as represented by the 2# Administrator of the National Aeronautics and Space Administration. 3# All Rights Reserved. 4# 5# Copyright 2012 OpenStack Foundation 6# Copyright 2012 Nebula, Inc. 7# 8# Licensed under the Apache License, Version 2.0 (the "License"); you may 9# not use this file except in compliance with the License. You may obtain 10# a copy of the License at 11# 12# http://www.apache.org/licenses/LICENSE-2.0 13# 14# Unless required by applicable law or agreed to in writing, software 15# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 16# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 17# License for the specific language governing permissions and limitations 18# under the License. 19 20import importlib 21 22from django.conf import settings 23from django.contrib.auth.models import User 24from django.core.exceptions import ImproperlyConfigured 25from django.test.utils import override_settings 26from django import urls 27 28import horizon 29from horizon import base 30from horizon import conf 31from horizon.test import helpers as test 32from horizon.test.test_dashboards.cats.dashboard import Cats 33from horizon.test.test_dashboards.cats.kittens.panel import Kittens 34from horizon.test.test_dashboards.cats.tigers.panel import Tigers 35from horizon.test.test_dashboards.dogs.dashboard import Dogs 36from horizon.test.test_dashboards.dogs.puppies.panel import Puppies 37 38 39class MyDash(horizon.Dashboard): 40 name = "My Dashboard" 41 slug = "mydash" 42 default_panel = "myslug" 43 44 45class MyOtherDash(horizon.Dashboard): 46 name = "My Other Dashboard" 47 slug = "mydash2" 48 default_panel = "myslug2" 49 50 51class MyPanel(horizon.Panel): 52 name = "My Panel" 53 slug = "myslug" 54 urls = 'horizon.test.test_dashboards.cats.kittens.urls' 55 56 57class RbacNoAccessPanel(horizon.Panel): 58 name = "RBAC Panel No" 59 slug = "rbac_panel_no" 60 61 def allowed(self, context): 62 return False 63 64 65class RbacYesAccessPanel(horizon.Panel): 66 name = "RBAC Panel Yes" 67 slug = "rbac_panel_yes" 68 69 70class BaseHorizonTests(test.TestCase): 71 72 def setUp(self): 73 super().setUp() 74 # Adjust our horizon config and register our custom dashboards/panels. 75 self.old_default_dash = settings.HORIZON_CONFIG['default_dashboard'] 76 settings.HORIZON_CONFIG['default_dashboard'] = 'cats' 77 self.old_dashboards = settings.HORIZON_CONFIG['dashboards'] 78 settings.HORIZON_CONFIG['dashboards'] = ('cats', 'dogs') 79 base.Horizon.register(Cats) 80 base.Horizon.register(Dogs) 81 Cats.register(Kittens) 82 Cats.register(Tigers) 83 Dogs.register(Puppies) 84 # Trigger discovery, registration, and URLconf generation if it 85 # hasn't happened yet. 86 base.Horizon._urls() 87 # Store our original dashboards 88 self._discovered_dashboards = list(base.Horizon._registry) 89 # Gather up and store our original panels for each dashboard 90 self._discovered_panels = {} 91 for dash in self._discovered_dashboards: 92 panels = list(base.Horizon._registry[dash]._registry) 93 self._discovered_panels[dash] = panels 94 95 def tearDown(self): 96 super().tearDown() 97 # Restore our settings 98 settings.HORIZON_CONFIG['default_dashboard'] = self.old_default_dash 99 settings.HORIZON_CONFIG['dashboards'] = self.old_dashboards 100 # Destroy our singleton and re-create it. 101 base.HorizonSite._instance = None 102 del base.Horizon 103 base.Horizon = base.HorizonSite() 104 # Reload the convenience references to Horizon stored in __init__ 105 importlib.reload(importlib.import_module("horizon")) 106 # Re-register our original dashboards and panels. 107 # This is necessary because autodiscovery only works on the first 108 # import, and calling reload introduces innumerable additional 109 # problems. Manual re-registration is the only good way for testing. 110 self._discovered_dashboards.remove(Cats) 111 self._discovered_dashboards.remove(Dogs) 112 for dash in self._discovered_dashboards: 113 base.Horizon.register(dash) 114 for panel in self._discovered_panels[dash]: 115 dash.register(panel) 116 117 def _reload_urls(self): 118 """Clears out the URL caches, and reloads the root urls module. 119 120 It re-triggers the autodiscovery mechanism for Horizon. 121 Allows URLs to be re-calculated after registering new dashboards. 122 Useful only for testing and should never be used on a live site. 123 """ 124 urls.clear_url_caches() 125 importlib.reload(importlib.import_module(settings.ROOT_URLCONF)) 126 base.Horizon._urls() 127 128 129class HorizonTests(BaseHorizonTests): 130 131 def test_registry(self): 132 """Verify registration and autodiscovery work correctly. 133 134 Please note that this implicitly tests that autodiscovery works 135 by virtue of the fact that the dashboards listed in 136 ``settings.INSTALLED_APPS`` are loaded from the start. 137 """ 138 # Registration 139 self.assertEqual(2, len(base.Horizon._registry)) 140 horizon.register(MyDash) 141 self.assertEqual(3, len(base.Horizon._registry)) 142 with self.assertRaises(ValueError): 143 horizon.register(MyPanel) 144 with self.assertRaises(ValueError): 145 horizon.register("MyPanel") 146 147 # Retrieval 148 my_dash_instance_by_name = horizon.get_dashboard("mydash") 149 self.assertIsInstance(my_dash_instance_by_name, MyDash) 150 my_dash_instance_by_class = horizon.get_dashboard(MyDash) 151 self.assertEqual(my_dash_instance_by_name, my_dash_instance_by_class) 152 with self.assertRaises(base.NotRegistered): 153 horizon.get_dashboard("fake") 154 self.assertQuerysetEqual(horizon.get_dashboards(), 155 ['<Dashboard: cats>', 156 '<Dashboard: dogs>', 157 '<Dashboard: mydash>']) 158 159 # Removal 160 self.assertEqual(3, len(base.Horizon._registry)) 161 horizon.unregister(MyDash) 162 self.assertEqual(2, len(base.Horizon._registry)) 163 with self.assertRaises(base.NotRegistered): 164 horizon.get_dashboard(MyDash) 165 166 def test_registry_two_dashboards(self): 167 "Verify registration of 2 dashboards" 168 169 # Registration 170 self.assertEqual(2, len(base.Horizon._registry)) 171 horizon.register(MyDash) 172 horizon.register(MyOtherDash) 173 self.assertEqual(4, len(base.Horizon._registry)) 174 175 # Retrieval 176 self.assertQuerysetEqual(horizon.get_dashboards(), 177 ['<Dashboard: cats>', 178 '<Dashboard: dogs>', 179 '<Dashboard: mydash>', 180 '<Dashboard: mydash2>']) 181 182 # Removal 183 self.assertEqual(4, len(base.Horizon._registry)) 184 horizon.unregister(MyDash) 185 horizon.unregister(MyOtherDash) 186 self.assertEqual(2, len(base.Horizon._registry)) 187 with self.assertRaises(base.NotRegistered): 188 horizon.get_dashboard(MyDash) 189 with self.assertRaises(base.NotRegistered): 190 horizon.get_dashboard(MyOtherDash) 191 192 def test_site(self): 193 self.assertEqual("Horizon", str(base.Horizon)) 194 self.assertEqual("<Site: horizon>", repr(base.Horizon)) 195 dash = base.Horizon.get_dashboard('cats') 196 self.assertEqual(dash, base.Horizon.get_default_dashboard()) 197 test_user = User() 198 self.assertEqual(dash.get_absolute_url(), 199 base.Horizon.get_user_home(test_user)) 200 201 def test_dashboard(self): 202 cats = horizon.get_dashboard("cats") 203 self.assertEqual(base.Horizon, cats._registered_with) 204 self.assertQuerysetEqual(cats.get_panels(), 205 ['<Panel: kittens>', 206 '<Panel: tigers>']) 207 self.assertEqual("/cats/", cats.get_absolute_url()) 208 self.assertEqual("Cats", cats.name) 209 210 # Test registering a module with a dashboard that defines panels 211 # as a panel group. 212 cats.register(MyPanel) 213 self.assertQuerysetEqual(cats.get_panel_groups()['other'], 214 ['<Panel: myslug>']) 215 # Test that panels defined as a tuple still return a PanelGroup 216 dogs = horizon.get_dashboard("dogs") 217 self.assertQuerysetEqual(dogs.get_panel_groups().values(), 218 ['<PanelGroup: default>']) 219 220 # Test registering a module with a dashboard that defines panels 221 # as a tuple. 222 dogs = horizon.get_dashboard("dogs") 223 dogs.register(MyPanel) 224 self.assertQuerysetEqual(dogs.get_panels(), 225 ['<Panel: puppies>', 226 '<Panel: myslug>']) 227 cats.unregister(MyPanel) 228 dogs.unregister(MyPanel) 229 230 def test_panels(self): 231 cats = horizon.get_dashboard("cats") 232 tigers = cats.get_panel("tigers") 233 self.assertEqual(cats, tigers._registered_with) 234 self.assertEqual("/cats/tigers/", tigers.get_absolute_url()) 235 236 def test_panel_without_slug_fails(self): 237 class InvalidPanel(horizon.Panel): 238 name = 'Invalid' 239 240 self.assertRaises(ImproperlyConfigured, InvalidPanel) 241 242 def test_registry_without_registerable_class_attr_fails(self): 243 class InvalidRegistry(base.Registry): 244 pass 245 246 self.assertRaises(ImproperlyConfigured, InvalidRegistry) 247 248 def test_index_url_name(self): 249 cats = horizon.get_dashboard("cats") 250 tigers = cats.get_panel("tigers") 251 tigers.index_url_name = "does_not_exist" 252 with self.assertRaises(urls.NoReverseMatch): 253 tigers.get_absolute_url() 254 tigers.index_url_name = "index" 255 self.assertEqual("/cats/tigers/", tigers.get_absolute_url()) 256 257 def test_lazy_urls(self): 258 urlpatterns = horizon.urls[0] 259 self.assertIsInstance(urlpatterns, base.LazyURLPattern) 260 # The following two methods simply should not raise any exceptions 261 iter(urlpatterns) 262 reversed(urlpatterns) 263 264 def test_horizon_test_isolation_1(self): 265 """Isolation Test Part 1: sets a value.""" 266 cats = horizon.get_dashboard("cats") 267 cats.evil = True 268 269 def test_horizon_test_isolation_2(self): 270 """Isolation Test Part 2: The value set in part 1 should be gone.""" 271 cats = horizon.get_dashboard("cats") 272 self.assertFalse(hasattr(cats, "evil")) 273 274 def test_public(self): 275 dogs = horizon.get_dashboard("dogs") 276 # Known to have no restrictions on it other than being logged in. 277 puppies = dogs.get_panel("puppies") 278 url = puppies.get_absolute_url() 279 280 # Get a clean, logged out client instance. 281 self.client.logout() 282 283 resp = self.client.get(url) 284 redirect_url = "?".join(['http://testserver' + settings.LOGIN_URL, 285 "next=%s" % url]) 286 self.assertRedirects(resp, redirect_url) 287 288 # Simulate ajax call 289 resp = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') 290 # Response should be HTTP 401 with redirect header 291 self.assertEqual(401, resp.status_code) 292 self.assertEqual(redirect_url, 293 resp["X-Horizon-Location"]) 294 295 @override_settings(SESSION_REFRESH=False) 296 def test_required_permissions(self): 297 dash = horizon.get_dashboard("cats") 298 panel = dash.get_panel('tigers') 299 300 # Non-admin user 301 self.assertQuerysetEqual(self.user.get_all_permissions(), []) 302 303 resp = self.client.get(panel.get_absolute_url()) 304 self.assertEqual(403, resp.status_code) 305 306 resp = self.client.get(panel.get_absolute_url(), 307 follow=False, 308 HTTP_X_REQUESTED_WITH='XMLHttpRequest') 309 self.assertEqual(403, resp.status_code) 310 311 # Test insufficient permissions for logged-in user 312 resp = self.client.get(panel.get_absolute_url(), follow=True) 313 self.assertEqual(403, resp.status_code) 314 self.assertTemplateUsed(resp, "not_authorized.html") 315 316 # Set roles for admin user 317 self.set_permissions(permissions=['test']) 318 319 resp = self.client.get(panel.get_absolute_url()) 320 self.assertEqual(200, resp.status_code) 321 322 # Test modal form 323 resp = self.client.get(panel.get_absolute_url(), 324 follow=False, 325 HTTP_X_REQUESTED_WITH='XMLHttpRequest') 326 self.assertEqual(200, resp.status_code) 327 328 def test_ssl_redirect_by_proxy(self): 329 dogs = horizon.get_dashboard("dogs") 330 puppies = dogs.get_panel("puppies") 331 url = puppies.get_absolute_url() 332 redirect_url = "?".join([settings.LOGIN_URL, 333 "next=%s" % url]) 334 335 self.client.logout() 336 resp = self.client.get(url) 337 self.assertRedirects(resp, settings.TESTSERVER + redirect_url) 338 339 # Set SSL settings for test server 340 settings.SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 341 'https') 342 343 resp = self.client.get(url, HTTP_X_FORWARDED_PROTOCOL="https") 344 self.assertEqual(302, resp.status_code) 345 self.assertEqual('https://testserver:80%s' % redirect_url, 346 resp['location']) 347 348 # Restore settings 349 settings.SECURE_PROXY_SSL_HEADER = None 350 351 352class GetUserHomeTests(test.TestCase): 353 """Test get_user_home parameters.""" 354 355 def setUp(self): 356 self.orig_user_home = settings.HORIZON_CONFIG['user_home'] 357 super().setUp() 358 self.original_username = "testname" 359 self.test_user = User() 360 self.test_user.username = self.original_username 361 362 def tearDown(self): 363 settings.HORIZON_CONFIG['user_home'] = self.orig_user_home 364 conf.HORIZON_CONFIG._setup() 365 super().tearDown() 366 367 def test_using_callable(self): 368 def themable_user_fnc(user): 369 return user.username.upper() 370 371 settings.HORIZON_CONFIG['user_home'] = themable_user_fnc 372 conf.HORIZON_CONFIG._setup() 373 374 self.assertEqual(self.test_user.username.upper(), 375 base.Horizon.get_user_home(self.test_user)) 376 377 def test_using_module_function(self): 378 module_func = 'django.utils.encoding.force_text' 379 settings.HORIZON_CONFIG['user_home'] = module_func 380 conf.HORIZON_CONFIG._setup() 381 382 self.test_user.username = 'testname' 383 self.assertEqual(self.original_username, 384 base.Horizon.get_user_home(self.test_user)) 385 386 def test_using_url(self): 387 fixed_url = "/url" 388 settings.HORIZON_CONFIG['user_home'] = fixed_url 389 conf.HORIZON_CONFIG._setup() 390 391 self.assertEqual(fixed_url, 392 base.Horizon.get_user_home(self.test_user)) 393 394 395class CustomPanelTests(BaseHorizonTests): 396 397 """Test customization of dashboards and panels. 398 399 This tests customization using 'customization_module' to HORIZON_CONFIG. 400 """ 401 402 def setUp(self): 403 super().setUp() 404 settings.HORIZON_CONFIG['customization_module'] = \ 405 'horizon.test.customization.cust_test1' 406 # refresh config 407 conf.HORIZON_CONFIG._setup() 408 self._reload_urls() 409 410 def tearDown(self): 411 # Restore dash 412 cats = horizon.get_dashboard("cats") 413 cats.name = "Cats" 414 horizon.register(Dogs) 415 self._discovered_dashboards.append(Dogs) 416 Dogs.register(Puppies) 417 Cats.register(Tigers) 418 super().tearDown() 419 settings.HORIZON_CONFIG.pop('customization_module') 420 # refresh config 421 conf.HORIZON_CONFIG._setup() 422 423 def test_customize_dashboard(self): 424 cats = horizon.get_dashboard("cats") 425 self.assertEqual("WildCats", cats.name) 426 self.assertQuerysetEqual(cats.get_panels(), 427 ['<Panel: kittens>']) 428 with self.assertRaises(base.NotRegistered): 429 horizon.get_dashboard("dogs") 430 431 432class CustomPermissionsTests(BaseHorizonTests): 433 434 """Test customization of permissions on panels. 435 436 This tests customization using 'customization_module' to HORIZON_CONFIG. 437 """ 438 439 def setUp(self): 440 settings.HORIZON_CONFIG['customization_module'] = \ 441 'horizon.test.customization.cust_test2' 442 # refresh config 443 conf.HORIZON_CONFIG._setup() 444 super().setUp() 445 446 def tearDown(self): 447 # Restore permissions 448 dogs = horizon.get_dashboard("dogs") 449 puppies = dogs.get_panel("puppies") 450 puppies.permissions = tuple([]) 451 super().tearDown() 452 settings.HORIZON_CONFIG.pop('customization_module') 453 # refresh config 454 conf.HORIZON_CONFIG._setup() 455 456 @override_settings(SESSION_REFRESH=False) 457 def test_customized_permissions(self): 458 dogs = horizon.get_dashboard("dogs") 459 panel = dogs.get_panel('puppies') 460 461 # Non-admin user 462 self.assertQuerysetEqual(self.user.get_all_permissions(), []) 463 464 resp = self.client.get(panel.get_absolute_url()) 465 self.assertEqual(403, resp.status_code) 466 467 resp = self.client.get(panel.get_absolute_url(), 468 follow=False, 469 HTTP_X_REQUESTED_WITH='XMLHttpRequest') 470 self.assertEqual(403, resp.status_code) 471 472 # Test customized permissions for logged-in user 473 resp = self.client.get(panel.get_absolute_url(), follow=True) 474 self.assertEqual(403, resp.status_code) 475 self.assertTemplateUsed(resp, "not_authorized.html") 476 477 # Set roles for admin user 478 self.set_permissions(permissions=['test']) 479 480 resp = self.client.get(panel.get_absolute_url()) 481 self.assertEqual(200, resp.status_code) 482 483 # Test modal form 484 resp = self.client.get(panel.get_absolute_url(), 485 follow=False, 486 HTTP_X_REQUESTED_WITH='XMLHttpRequest') 487 self.assertEqual(resp.status_code, 200) 488 489 490class RbacHorizonTests(test.TestCase): 491 492 def setUp(self): 493 super().setUp() 494 # Adjust our horizon config and register our custom dashboards/panels. 495 self.old_default_dash = settings.HORIZON_CONFIG['default_dashboard'] 496 settings.HORIZON_CONFIG['default_dashboard'] = 'cats' 497 self.old_dashboards = settings.HORIZON_CONFIG['dashboards'] 498 settings.HORIZON_CONFIG['dashboards'] = ('cats', 'dogs') 499 base.Horizon.register(Cats) 500 base.Horizon.register(Dogs) 501 Cats.register(RbacNoAccessPanel) 502 Cats.default_panel = 'rbac_panel_no' 503 Dogs.register(RbacYesAccessPanel) 504 Dogs.default_panel = 'rbac_panel_yes' 505 # Trigger discovery, registration, and URLconf generation if it 506 # hasn't happened yet. 507 base.Horizon._urls() 508 # Store our original dashboards 509 self._discovered_dashboards = list(base.Horizon._registry) 510 # Gather up and store our original panels for each dashboard 511 self._discovered_panels = {} 512 for dash in self._discovered_dashboards: 513 panels = list(base.Horizon._registry[dash]._registry) 514 self._discovered_panels[dash] = panels 515 516 def tearDown(self): 517 super().tearDown() 518 # Restore our settings 519 settings.HORIZON_CONFIG['default_dashboard'] = self.old_default_dash 520 settings.HORIZON_CONFIG['dashboards'] = self.old_dashboards 521 # Destroy our singleton and re-create it. 522 base.HorizonSite._instance = None 523 del base.Horizon 524 base.Horizon = base.HorizonSite() 525 # Reload the convenience references to Horizon stored in __init__ 526 importlib.reload(importlib.import_module("horizon")) 527 528 # Reset Cats and Dogs default_panel to default values 529 Cats.default_panel = 'kittens' 530 Dogs.default_panel = 'puppies' 531 532 # Re-register our original dashboards and panels. 533 # This is necessary because autodiscovery only works on the first 534 # import, and calling reload introduces innumerable additional 535 # problems. Manual re-registration is the only good way for testing. 536 self._discovered_dashboards.remove(Cats) 537 self._discovered_dashboards.remove(Dogs) 538 for dash in self._discovered_dashboards: 539 base.Horizon.register(dash) 540 for panel in self._discovered_panels[dash]: 541 dash.register(panel) 542 543 def test_rbac_panels(self): 544 context = {'request': self.request} 545 cats = horizon.get_dashboard("cats") 546 self.assertEqual(cats._registered_with, base.Horizon) 547 self.assertQuerysetEqual(cats.get_panels(), 548 ['<Panel: rbac_panel_no>']) 549 self.assertFalse(cats.can_access(context)) 550 551 dogs = horizon.get_dashboard("dogs") 552 self.assertEqual(dogs._registered_with, base.Horizon) 553 self.assertQuerysetEqual(dogs.get_panels(), 554 ['<Panel: rbac_panel_yes>']) 555 556 self.assertTrue(dogs.can_access(context)) 557