1# encoding=utf-8 2# 3# Copyright 2012 Nebula, Inc. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); you may 6# not use this file except in compliance with the License. You may obtain 7# a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14# License for the specific language governing permissions and limitations 15# under the License. 16 17import copy 18 19from django.conf import settings 20from django import http 21from django.test.utils import override_settings 22 23from horizon import exceptions 24from horizon import middleware 25from horizon import tabs as horizon_tabs 26from horizon.test import helpers as test 27 28from horizon.test.unit.tables.test_tables import MyTable 29from horizon.test.unit.tables.test_tables import TEST_DATA 30 31 32class BaseTestTab(horizon_tabs.Tab): 33 def get_context_data(self, request): 34 return {"tab": self} 35 36 37class TabOne(BaseTestTab): 38 slug = "tab_one" 39 name = "Tab One" 40 template_name = "_tab.html" 41 42 43class TabDelayed(BaseTestTab): 44 slug = "tab_delayed" 45 name = "Delayed Tab" 46 template_name = "_tab.html" 47 preload = False 48 49 50class TabDisabled(BaseTestTab): 51 slug = "tab_disabled" 52 name = "Disabled Tab" 53 template_name = "_tab.html" 54 55 def enabled(self, request): 56 return False 57 58 59class TabDisallowed(BaseTestTab): 60 slug = "tab_disallowed" 61 name = "Disallowed Tab" 62 template_name = "_tab.html" 63 64 def allowed(self, request): 65 return False 66 67 68class TabWithPolicy(BaseTestTab): 69 slug = "tab_with_policy" 70 name = "tab only visible to admin" 71 template_name = "_tab.html" 72 policy_rules = (("compute", "role:admin"),) 73 74 75class Group(horizon_tabs.TabGroup): 76 slug = "tab_group" 77 tabs = (TabOne, TabDelayed, TabDisabled, TabDisallowed, TabWithPolicy) 78 sticky = True 79 80 def tabs_not_available(self): 81 self._assert_tabs_not_available = True 82 83 84class GroupWithConfig(horizon_tabs.TabGroup): 85 slug = "tab_group" 86 tabs = (TabOne, TabDisallowed) 87 sticky = True 88 89 90class TabWithTable(horizon_tabs.TableTab): 91 table_classes = (MyTable,) 92 name = "Tab With My Table" 93 slug = "tab_with_table" 94 template_name = "horizon/common/_detail_table.html" 95 96 def get_my_table_data(self): 97 return TEST_DATA 98 99 100class RecoverableErrorTab(horizon_tabs.Tab): 101 name = "Recoverable Error Tab" 102 slug = "recoverable_error_tab" 103 template_name = "_tab.html" 104 105 def get_context_data(self, request): 106 # Raise a known recoverable error. 107 exc = exceptions.AlreadyExists("Recoverable!", horizon_tabs.Tab) 108 exc.silence_logging = True 109 raise exc 110 111 112class RedirectExceptionTab(horizon_tabs.Tab): 113 name = "Redirect Exception Tab" 114 slug = "redirect_exception_tab" 115 template_name = "_tab.html" 116 url = settings.TESTSERVER + settings.LOGIN_URL 117 118 def get_context_data(self, request): 119 # Raise a known recoverable error. 120 exc = exceptions.Http302(self.url) 121 exc.silence_logging = True 122 raise exc 123 124 125class TableTabGroup(horizon_tabs.TabGroup): 126 slug = "tab_group" 127 tabs = [TabWithTable] 128 129 130class TabWithTableView(horizon_tabs.TabbedTableView): 131 tab_group_class = TableTabGroup 132 template_name = "tab_group.html" 133 134 135class TabTests(test.TestCase): 136 @override_settings(POLICY_CHECK_FUNCTION=lambda *args: True) 137 def test_tab_group_basics(self): 138 tg = Group(self.request) 139 140 # Test tab instantiation/attachment to tab group, and get_tabs method 141 tabs = tg.get_tabs() 142 # "tab_disallowed" should NOT be in this list. 143 # "tab_with_policy" should be present, since our policy check 144 # always passes 145 self.assertQuerysetEqual(tabs, ['<TabOne: tab_one>', 146 '<TabDelayed: tab_delayed>', 147 '<TabDisabled: tab_disabled>', 148 '<TabWithPolicy: tab_with_policy>']) 149 # Test get_id 150 self.assertEqual("tab_group", tg.get_id()) 151 # get_default_classes 152 self.assertEqual(horizon_tabs.base.CSS_TAB_GROUP_CLASSES, 153 tg.get_default_classes()) 154 # Test get_tab 155 self.assertEqual("tab_one", tg.get_tab("tab_one").slug) 156 157 # Test selected is None w/o GET input 158 self.assertIsNone(tg.selected) 159 160 # Test get_selected_tab is None w/o GET input 161 self.assertIsNone(tg.get_selected_tab()) 162 163 @override_settings(POLICY_CHECK_FUNCTION=lambda *args: False) 164 def test_failed_tab_policy(self): 165 tg = Group(self.request) 166 167 # Test tab instantiation/attachment to tab group, and get_tabs method 168 tabs = tg.get_tabs() 169 # "tab_disallowed" should NOT be in this list, it's not allowed 170 # "tab_with_policy" should also not be present as its 171 # policy check failed 172 self.assertQuerysetEqual(tabs, ['<TabOne: tab_one>', 173 '<TabDelayed: tab_delayed>', 174 '<TabDisabled: tab_disabled>']) 175 176 @test.update_settings( 177 HORIZON_CONFIG={'extra_tabs': { 178 'horizon.test.unit.tabs.test_tabs.GroupWithConfig': ( 179 (2, 'horizon.test.unit.tabs.test_tabs.TabDelayed'), 180 # No priority means priority 0 181 'horizon.test.unit.tabs.test_tabs.TabDisabled', 182 ), 183 }} 184 ) 185 def test_tab_group_with_config(self): 186 tg = GroupWithConfig(self.request) 187 tabs = tg.get_tabs() 188 # "tab_disallowed" should NOT be in this list. 189 # Other tabs must be ordered in the priorities in HORIZON_CONFIG. 190 self.assertQuerysetEqual(tabs, ['<TabOne: tab_one>', 191 '<TabDisabled: tab_disabled>', 192 '<TabDelayed: tab_delayed>']) 193 194 def test_tab_group_active_tab(self): 195 tg = Group(self.request) 196 197 # active tab w/o selected 198 self.assertEqual(tg.get_tabs()[0], tg.active) 199 200 # active tab w/ selected 201 self.request.GET['tab'] = "tab_group__tab_delayed" 202 tg = Group(self.request) 203 self.assertEqual(tg.get_tab('tab_delayed'), tg.active) 204 205 # active tab w/ invalid selected 206 self.request.GET['tab'] = "tab_group__tab_invalid" 207 tg = Group(self.request) 208 self.assertEqual(tg.get_tabs()[0], tg.active) 209 210 # active tab w/ disallowed selected 211 self.request.GET['tab'] = "tab_group__tab_disallowed" 212 tg = Group(self.request) 213 self.assertEqual(tg.get_tabs()[0], tg.active) 214 215 # active tab w/ disabled selected 216 self.request.GET['tab'] = "tab_group__tab_disabled" 217 tg = Group(self.request) 218 self.assertEqual(tg.get_tabs()[0], tg.active) 219 220 # active tab w/ non-empty garbage selected 221 # Note: this entry does not contain the '__' SEPARATOR string. 222 self.request.GET['tab'] = "<!--" 223 tg = Group(self.request) 224 self.assertEqual(tg.get_tabs()[0], tg.active) 225 226 def test_tab_basics(self): 227 tg = Group(self.request) 228 tab_one = tg.get_tab("tab_one") 229 tab_delayed = tg.get_tab("tab_delayed") 230 tab_disabled = tg.get_tab("tab_disabled", allow_disabled=True) 231 232 # Disallowed tab isn't even returned 233 tab_disallowed = tg.get_tab("tab_disallowed") 234 self.assertIsNone(tab_disallowed) 235 236 # get_id 237 self.assertEqual("tab_group__tab_one", tab_one.get_id()) 238 239 # get_default_classes 240 self.assertEqual(horizon_tabs.base.CSS_ACTIVE_TAB_CLASSES, 241 tab_one.get_default_classes()) 242 self.assertEqual(horizon_tabs.base.CSS_DISABLED_TAB_CLASSES, 243 tab_disabled.get_default_classes()) 244 245 # load, allowed, enabled 246 self.assertTrue(tab_one.load) 247 self.assertFalse(tab_delayed.load) 248 self.assertFalse(tab_disabled.load) 249 self.request.GET['tab'] = tab_delayed.get_id() 250 tg = Group(self.request) 251 tab_delayed = tg.get_tab("tab_delayed") 252 self.assertTrue(tab_delayed.load) 253 254 # is_active 255 self.request.GET['tab'] = "" 256 tg = Group(self.request) 257 tab_one = tg.get_tab("tab_one") 258 tab_delayed = tg.get_tab("tab_delayed") 259 self.assertTrue(tab_one.is_active()) 260 self.assertFalse(tab_delayed.is_active()) 261 262 self.request.GET['tab'] = tab_delayed.get_id() 263 tg = Group(self.request) 264 tab_one = tg.get_tab("tab_one") 265 tab_delayed = tg.get_tab("tab_delayed") 266 self.assertFalse(tab_one.is_active()) 267 self.assertTrue(tab_delayed.is_active()) 268 269 def test_rendering(self): 270 tg = Group(self.request) 271 tab_one = tg.get_tab("tab_one") 272 tab_delayed = tg.get_tab("tab_delayed") 273 tab_disabled = tg.get_tab("tab_disabled", allow_disabled=True) 274 275 # tab group 276 output = tg.render() 277 res = http.HttpResponse(output.strip()) 278 self.assertContains(res, "<li", 4) 279 280 # stickiness 281 self.assertContains(res, 'data-sticky-tabs="sticky"', 1) 282 283 # tab 284 output = tab_one.render() 285 self.assertEqual(tab_one.name, output.strip()) 286 287 # disabled tab 288 output = tab_disabled.render() 289 self.assertEqual("", output.strip()) 290 291 # preload false 292 output = tab_delayed.render() 293 self.assertEqual("", output.strip()) 294 295 # preload false w/ active 296 self.request.GET['tab'] = tab_delayed.get_id() 297 tg = Group(self.request) 298 tab_delayed = tg.get_tab("tab_delayed") 299 output = tab_delayed.render() 300 self.assertEqual(tab_delayed.name, output.strip()) 301 302 def test_table_tabs(self): 303 tab_group = TableTabGroup(self.request) 304 tabs = tab_group.get_tabs() 305 # Only one tab, as expected. 306 self.assertEqual(1, len(tabs)) 307 tab = tabs[0] 308 # Make sure it's the tab we think it is. 309 self.assertIsInstance(tab, horizon_tabs.TableTab) 310 # Data should not be loaded yet. 311 self.assertFalse(tab._table_data_loaded) 312 table = tab._tables[MyTable.Meta.name] 313 self.assertIsInstance(table, MyTable) 314 # Let's make sure the data *really* isn't loaded yet. 315 self.assertIsNone(table.data) 316 # Okay, load the data. 317 tab.load_table_data() 318 self.assertTrue(tab._table_data_loaded) 319 self.assertQuerysetEqual(table.data, 320 ['FakeObject: object_1', 321 'FakeObject: object_2', 322 'FakeObject: object_3', 323 'FakeObject: öbject_4'], 324 transform=str) 325 context = tab.get_context_data(self.request) 326 # Make sure our table is loaded into the context correctly 327 self.assertEqual(table, context['my_table_table']) 328 # Since we only had one table we should get the shortcut name too. 329 self.assertEqual(table, context['table']) 330 331 def test_tabbed_table_view(self): 332 view = TabWithTableView.as_view() 333 334 # Be sure we get back a rendered table containing data for a GET 335 req = self.factory.get("/") 336 res = view(req) 337 self.assertContains(res, "<table", 1) 338 self.assertContains(res, "Displaying 4 items", 2) 339 340 # AJAX response to GET for row update 341 params = {"table": "my_table", "action": "row_update", "obj_id": "1"} 342 req = self.factory.get('/', params, 343 HTTP_X_REQUESTED_WITH='XMLHttpRequest') 344 res = view(req) 345 self.assertEqual(200, res.status_code) 346 # Make sure we got back a row but not a table or body 347 self.assertContains(res, "<tr", 1) 348 self.assertContains(res, "<table", 0) 349 self.assertContains(res, "<body", 0) 350 351 # Response to POST for table action 352 action_string = "my_table__toggle__2" 353 req = self.factory.post('/', {'action': action_string}) 354 res = view(req) 355 self.assertEqual(302, res.status_code) 356 self.assertEqual("/", res["location"]) 357 358 # Ensure that lookup errors are raised as such instead of converted 359 # to TemplateSyntaxErrors. 360 action_string = "my_table__toggle__2000000000" 361 req = self.factory.post('/', {'action': action_string}) 362 self.assertRaises(exceptions.Http302, view, req) 363 364 365class TabExceptionTests(test.TestCase): 366 def setUp(self): 367 super().setUp() 368 self._original_tabs = copy.copy(TabWithTableView.tab_group_class.tabs) 369 370 def tearDown(self): 371 super().tearDown() 372 TabWithTableView.tab_group_class.tabs = self._original_tabs 373 374 @override_settings(SESSION_REFRESH=False) 375 def test_tab_view_exception(self): 376 TabWithTableView.tab_group_class.tabs.append(RecoverableErrorTab) 377 view = TabWithTableView.as_view() 378 req = self.factory.get("/") 379 res = view(req) 380 self.assertMessageCount(res, error=1) 381 382 @override_settings(SESSION_REFRESH=False) 383 def test_tab_302_exception(self): 384 TabWithTableView.tab_group_class.tabs.append(RedirectExceptionTab) 385 view = TabWithTableView.as_view() 386 req = self.factory.get("/") 387 mw = middleware.HorizonMiddleware('dummy_get_response') 388 try: 389 resp = view(req) 390 except Exception as e: 391 resp = mw.process_exception(req, e) 392 resp.client = self.client 393 self.assertRedirects(resp, RedirectExceptionTab.url) 394