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