tests.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # Licensed to Cloudera, Inc. under one
  4. # or more contributor license agreements. See the NOTICE file
  5. # distributed with this work for additional information
  6. # regarding copyright ownership. Cloudera, Inc. licenses this file
  7. # to you under the Apache License, Version 2.0 (the
  8. # "License"); you may not use this file except in compliance
  9. # with the License. You may obtain a copy of the License at
  10. #
  11. # http://www.apache.org/licenses/LICENSE-2.0
  12. #
  13. # Unless required by applicable law or agreed to in writing, software
  14. # distributed under the License is distributed on an "AS IS" BASIS,
  15. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. # See the License for the specific language governing permissions and
  17. # limitations under the License.
  18. import desktop
  19. import desktop.urls
  20. import desktop.conf
  21. import logging
  22. import os
  23. import time
  24. import desktop.views as views
  25. import proxy.conf
  26. from nose.plugins.attrib import attr
  27. from nose.tools import assert_true, assert_equal, assert_not_equal, assert_raises
  28. from django.conf.urls.defaults import patterns, url
  29. from django.core.urlresolvers import reverse
  30. from django.http import HttpResponse
  31. from django.db.models import query, CharField, SmallIntegerField
  32. from desktop.lib import django_mako
  33. from desktop.lib.django_test_util import make_logged_in_client
  34. from desktop.lib.paginator import Paginator
  35. from desktop.lib.conf import validate_path
  36. from desktop.lib.django_util import TruncatingModel
  37. from desktop.lib.exceptions_renderable import PopupException
  38. from desktop.lib.test_utils import grant_access
  39. from desktop.views import check_config_ajax
  40. def setup_test_environment():
  41. """
  42. Sets up mako to signal template rendering.
  43. """
  44. django_mako.render_to_string = django_mako.render_to_string_test
  45. setup_test_environment.__test__ = False
  46. def teardown_test_environment():
  47. """
  48. This method is called by nose_runner when
  49. the tests all finish. This helps track
  50. down when tests aren't cleaning up after
  51. themselves and leaving threads hanging around.
  52. """
  53. import threading
  54. # We should shut down all relevant threads by test completion.
  55. threads = list(threading.enumerate())
  56. try:
  57. import threadframe
  58. import traceback
  59. if len(threads) > 1:
  60. for v in threadframe.dict().values():
  61. traceback.print_stack(v)
  62. finally:
  63. # threadframe is only available in the dev build.
  64. pass
  65. assert 1 == len(threads), threads
  66. django_mako.render_to_string = django_mako.render_to_string_normal
  67. teardown_test_environment.__test__ = False
  68. def test_log_view():
  69. c = make_logged_in_client()
  70. URL = reverse(views.log_view)
  71. LOG = logging.getLogger(__name__)
  72. LOG.warn('une voix m’a réveillé')
  73. # UnicodeDecodeError: 'ascii' codec can't decode byte... should not happen
  74. response = c.get(URL)
  75. assert_equal(200, response.status_code)
  76. c = make_logged_in_client()
  77. URL = reverse(views.log_view)
  78. LOG = logging.getLogger(__name__)
  79. LOG.warn('Got response: PK\x03\x04\n\x00\x00\x08\x00\x00\xad\x0cN?\x00\x00\x00\x00')
  80. # DjangoUnicodeDecodeError: 'utf8' codec can't decode byte 0xad in position 75: invalid start byte... should not happen
  81. response = c.get(URL)
  82. assert_equal(200, response.status_code)
  83. def test_dump_config():
  84. c = make_logged_in_client()
  85. CANARY = "abracadabra"
  86. clear = desktop.conf.HTTP_HOST.set_for_testing(CANARY)
  87. response1 = c.get('/dump_config')
  88. assert_true(CANARY in response1.content)
  89. response2 = c.get('/dump_config', dict(private="true"))
  90. assert_true(CANARY in response2.content)
  91. # There are more private variables...
  92. assert_true(len(response1.content) < len(response2.content))
  93. clear()
  94. CANARY = "(localhost|127\.0\.0\.1):(50030|50070|50060|50075)"
  95. clear = proxy.conf.WHITELIST.set_for_testing(CANARY)
  96. response1 = c.get('/dump_config')
  97. assert_true(CANARY in response1.content)
  98. clear()
  99. # Malformed port per HUE-674
  100. CANARY = "asdfoijaoidfjaosdjffjfjaoojosjfiojdosjoidjfoa"
  101. clear = desktop.conf.HTTP_PORT.set_for_testing(CANARY)
  102. response1 = c.get('/dump_config')
  103. assert_true(CANARY in response1.content, response1.content)
  104. clear()
  105. CANARY = '/tmp/spacé.dat'
  106. finish = proxy.conf.WHITELIST.set_for_testing(CANARY)
  107. try:
  108. response = c.get('/dump_config')
  109. assert_true(CANARY in response.content, response.content)
  110. finally:
  111. finish()
  112. # Login as someone else
  113. client_not_me = make_logged_in_client(username='not_me', is_superuser=False, groupname='test')
  114. grant_access("not_me", "test", "desktop")
  115. response = client_not_me.get('/dump_config')
  116. assert_equal("You must be a superuser.", response.content)
  117. os.environ["HUE_CONF_DIR"] = "/tmp/test_hue_conf_dir"
  118. resp = c.get('/dump_config')
  119. del os.environ["HUE_CONF_DIR"]
  120. assert_true('/tmp/test_hue_conf_dir' in resp.content, resp)
  121. def test_prefs():
  122. c = make_logged_in_client()
  123. # Get everything
  124. response = c.get('/prefs/')
  125. assert_equal('{}', response.content)
  126. # Set and get
  127. response = c.get('/prefs/foo', dict(set="bar"))
  128. assert_equal('true', response.content)
  129. response = c.get('/prefs/foo')
  130. assert_equal('"bar"', response.content)
  131. # Reset (use post this time)
  132. c.post('/prefs/foo', dict(set="baz"))
  133. response = c.get('/prefs/foo')
  134. assert_equal('"baz"', response.content)
  135. # Check multiple values
  136. c.post('/prefs/elephant', dict(set="room"))
  137. response = c.get('/prefs/')
  138. assert_true("baz" in response.content)
  139. assert_true("room" in response.content)
  140. # Delete everything
  141. c.get('/prefs/elephant', dict(delete=""))
  142. c.get('/prefs/foo', dict(delete=""))
  143. response = c.get('/prefs/')
  144. assert_equal('{}', response.content)
  145. # Check non-existent value
  146. response = c.get('/prefs/doesNotExist')
  147. assert_equal('null', response.content)
  148. def test_status_bar():
  149. """
  150. Subs out the status_bar_views registry with temporary examples.
  151. Tests handling of errors on view functions.
  152. """
  153. backup = views._status_bar_views
  154. views._status_bar_views = []
  155. c = make_logged_in_client()
  156. views.register_status_bar_view(lambda _: HttpResponse("foo", status=200))
  157. views.register_status_bar_view(lambda _: HttpResponse("bar"))
  158. views.register_status_bar_view(lambda _: None)
  159. def f(r):
  160. raise Exception()
  161. views.register_status_bar_view(f)
  162. response = c.get("/status_bar")
  163. assert_equal("foobar", response.content)
  164. views._status_bar_views = backup
  165. def test_paginator():
  166. """
  167. Test that the paginator works with partial list.
  168. """
  169. def assert_page(page, data, start, end):
  170. assert_equal(page.object_list, data)
  171. assert_equal(page.start_index(), start)
  172. assert_equal(page.end_index(), end)
  173. # First page 1-20
  174. obj = range(20)
  175. pgn = Paginator(obj, per_page=20, total=25)
  176. assert_page(pgn.page(1), obj, 1, 20)
  177. # Second page 21-25
  178. obj = range(5)
  179. pgn = Paginator(obj, per_page=20, total=25)
  180. assert_page(pgn.page(2), obj, 21, 25)
  181. # Handle extra data on first page (22 items on a 20-page)
  182. obj = range(22)
  183. pgn = Paginator(obj, per_page=20, total=25)
  184. assert_page(pgn.page(1), range(20), 1, 20)
  185. # Handle extra data on second page (22 items on a 20-page)
  186. obj = range(22)
  187. pgn = Paginator(obj, per_page=20, total=25)
  188. assert_page(pgn.page(2), range(5), 21, 25)
  189. # Handle total < len(obj). Only works for QuerySet.
  190. obj = query.QuerySet()
  191. obj._result_cache = range(10)
  192. pgn = Paginator(obj, per_page=10, total=9)
  193. assert_page(pgn.page(1), range(10), 1, 10)
  194. # Still works with a normal complete list
  195. obj = range(25)
  196. pgn = Paginator(obj, per_page=20)
  197. assert_page(pgn.page(1), range(20), 1, 20)
  198. assert_page(pgn.page(2), range(20, 25), 21, 25)
  199. def test_thread_dump():
  200. c = make_logged_in_client()
  201. response = c.get("/debug/threads")
  202. assert_true("test_thread_dump" in response.content)
  203. def test_truncating_model():
  204. class TinyModel(TruncatingModel):
  205. short_field = CharField(max_length=10)
  206. non_string_field = SmallIntegerField()
  207. a = TinyModel()
  208. a.short_field = 'a' * 9 # One less than it's max length
  209. assert_true(a.short_field == 'a' * 9, 'Short-enough field does not get truncated')
  210. a.short_field = 'a' * 11 # One more than it's max_length
  211. assert_true(a.short_field == 'a' * 10, 'Too-long field gets truncated')
  212. a.non_string_field = 10**10
  213. assert_true(a.non_string_field == 10**10, 'non-string fields are not truncated')
  214. def test_error_handling():
  215. restore_django_debug = desktop.conf.DJANGO_DEBUG_MODE.set_for_testing(False)
  216. restore_500_debug = desktop.conf.HTTP_500_DEBUG_MODE.set_for_testing(False)
  217. exc_msg = "error_raising_view: Test earráid handling"
  218. def error_raising_view(request, *args, **kwargs):
  219. raise Exception(exc_msg)
  220. def popup_exception_view(request, *args, **kwargs):
  221. raise PopupException(exc_msg, title="earráid", detail=exc_msg)
  222. # Add an error view
  223. error_url_pat = patterns('',
  224. url('^500_internal_error$', error_raising_view),
  225. url('^popup_exception$', popup_exception_view))
  226. desktop.urls.urlpatterns.extend(error_url_pat)
  227. try:
  228. def store_exc_info(*args, **kwargs):
  229. pass
  230. # Disable the test client's exception forwarding
  231. c = make_logged_in_client()
  232. c.store_exc_info = store_exc_info
  233. response = c.get('/500_internal_error')
  234. assert_true('500.mako' in response.template)
  235. assert_true('Thank you for your patience' in response.content)
  236. assert_true(exc_msg not in response.content)
  237. # Now test the 500 handler with backtrace
  238. desktop.conf.HTTP_500_DEBUG_MODE.set_for_testing(True)
  239. response = c.get('/500_internal_error')
  240. assert_equal(response.template.name, 'Technical 500 template')
  241. assert_true(exc_msg in response.content)
  242. # PopupException
  243. response = c.get('/popup_exception')
  244. assert_true('popup_error.mako' in response.template)
  245. assert_true(exc_msg in response.content)
  246. finally:
  247. # Restore the world
  248. for i in error_url_pat:
  249. desktop.urls.urlpatterns.remove(i)
  250. restore_django_debug()
  251. restore_500_debug()
  252. def test_error_handling_failure():
  253. # Change rewrite_user to call has_hue_permission
  254. # Try to get filebrowser page
  255. # test for default 500 page
  256. # Restore rewrite_user
  257. import desktop.auth.backend
  258. c = make_logged_in_client()
  259. restore_django_debug = desktop.conf.DJANGO_DEBUG_MODE.set_for_testing(False)
  260. restore_500_debug = desktop.conf.HTTP_500_DEBUG_MODE.set_for_testing(False)
  261. original_rewrite_user = desktop.auth.backend.rewrite_user
  262. def rewrite_user(user):
  263. user = original_rewrite_user(user)
  264. delattr(user, 'has_hue_permission')
  265. return user
  266. original_rewrite_user = desktop.auth.backend.rewrite_user
  267. desktop.auth.backend.rewrite_user = rewrite_user
  268. try:
  269. # Make sure we are showing default 500.html page.
  270. # See django.test.client#L246
  271. assert_raises(AttributeError, c.get, '/dump_config')
  272. finally:
  273. # Restore the world
  274. restore_django_debug()
  275. restore_500_debug()
  276. desktop.auth.backend.rewrite_user = original_rewrite_user
  277. def test_404_handling():
  278. view_name = '/the-view-that-is-not-there'
  279. c = make_logged_in_client()
  280. response = c.get(view_name)
  281. assert_true('404.mako' in response.template)
  282. assert_true('Not Found' in response.content)
  283. assert_true(view_name in response.content)
  284. class RecordingHandler(logging.Handler):
  285. def __init__(self, *args, **kwargs):
  286. logging.Handler.__init__(self, *args, **kwargs)
  287. self.records = []
  288. def emit(self, r):
  289. self.records.append(r)
  290. def test_log_event():
  291. c = make_logged_in_client()
  292. root = logging.getLogger("desktop.views.log_frontend_event")
  293. handler = RecordingHandler()
  294. root.addHandler(handler)
  295. c.get("/log_frontend_event?level=info&message=foo")
  296. assert_equal("INFO", handler.records[-1].levelname)
  297. assert_equal("Untrusted log event from user test: foo", handler.records[-1].message)
  298. assert_equal("desktop.views.log_frontend_event", handler.records[-1].name)
  299. c.get("/log_frontend_event?level=error&message=foo2")
  300. assert_equal("ERROR", handler.records[-1].levelname)
  301. assert_equal("Untrusted log event from user test: foo2", handler.records[-1].message)
  302. c.get("/log_frontend_event?message=foo3")
  303. assert_equal("INFO", handler.records[-1].levelname)
  304. assert_equal("Untrusted log event from user test: foo3", handler.records[-1].message)
  305. c.post("/log_frontend_event", {
  306. "message": "01234567" * 1024})
  307. assert_equal("INFO", handler.records[-1].levelname)
  308. assert_equal("Untrusted log event from user test: " + "01234567"*(1024/8),
  309. handler.records[-1].message)
  310. root.removeHandler(handler)
  311. def test_validate_path():
  312. reset = desktop.conf.SSL_PRIVATE_KEY.set_for_testing('/')
  313. assert_equal([], validate_path(desktop.conf.SSL_PRIVATE_KEY, is_dir=True))
  314. reset()
  315. reset = desktop.conf.SSL_PRIVATE_KEY.set_for_testing('/tmm/does_not_exist')
  316. assert_not_equal([], validate_path(desktop.conf.SSL_PRIVATE_KEY, is_dir=True))
  317. reset()
  318. @attr('requires_hadoop')
  319. def test_config_check():
  320. reset = (
  321. desktop.conf.SECRET_KEY.set_for_testing(''),
  322. desktop.conf.SSL_CERTIFICATE.set_for_testing('foobar'),
  323. desktop.conf.SSL_PRIVATE_KEY.set_for_testing(''),
  324. desktop.conf.DEFAULT_SITE_ENCODING.set_for_testing('klingon')
  325. )
  326. try:
  327. cli = make_logged_in_client()
  328. resp = cli.get('/debug/check_config')
  329. assert_true('Secret key should be configured' in resp.content, resp)
  330. assert_true('desktop.ssl_certificate' in resp.content, resp)
  331. assert_true('Path does not exist' in resp.content, resp)
  332. assert_true('SSL private key file should be set' in resp.content, resp)
  333. assert_true('klingon' in resp.content, resp)
  334. assert_true('Encoding not supported' in resp.content, resp)
  335. # Set HUE_CONF_DIR and make sure check_config returns appropriate conf
  336. os.environ["HUE_CONF_DIR"] = "/tmp/test_hue_conf_dir"
  337. resp = cli.get('/debug/check_config')
  338. del os.environ["HUE_CONF_DIR"]
  339. assert_true('/tmp/test_hue_conf_dir' in resp.content, resp)
  340. # Alert present in the status bar
  341. resp = cli.get('/about', follow=True)
  342. assert_true('misconfiguration' in resp.content, resp.content)
  343. finally:
  344. for old_conf in reset:
  345. old_conf()
  346. def test_last_access_time():
  347. c = make_logged_in_client(username="access_test")
  348. c.post('/accounts/login/')
  349. login = desktop.auth.views.get_current_users()
  350. before_access_time = time.time()
  351. response = c.post('/status_bar')
  352. after_access_time = time.time()
  353. access = desktop.auth.views.get_current_users()
  354. user = response.context['user']
  355. login_time = login[user]['time']
  356. access_time = access[user]['time']
  357. # Check that 'last_access_time' is later than login time
  358. assert_true(login_time < access_time)
  359. # Check that 'last_access_time' is in between the timestamps before and after the last access path
  360. assert_true(before_access_time < access_time)
  361. assert_true(access_time < after_access_time)
  362. def test_ui_customizations():
  363. custom_banner = 'test ui customization'
  364. reset = (
  365. desktop.conf.CUSTOM.BANNER_TOP_HTML.set_for_testing(custom_banner),
  366. )
  367. try:
  368. c = make_logged_in_client()
  369. resp = c.get('/about', follow=True)
  370. assert_true(custom_banner in resp.content, resp)
  371. finally:
  372. for old_conf in reset:
  373. old_conf()
  374. @attr('requires_hadoop')
  375. def test_check_config_ajax():
  376. c = make_logged_in_client()
  377. response = c.get(reverse(check_config_ajax))
  378. assert_true("Misconfiguration" in response.content, response.content)