tests.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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. from desktop.lib import django_mako
  19. from nose.tools import assert_true, assert_equal, assert_not_equal
  20. from desktop.lib.django_test_util import make_logged_in_client
  21. from django.conf.urls.defaults import patterns, url
  22. from django.core.urlresolvers import reverse
  23. from django.http import HttpResponse
  24. from django.db.models import query, CharField, SmallIntegerField
  25. from desktop.lib.paginator import Paginator
  26. from desktop.lib.conf import validate_path
  27. import desktop
  28. import desktop.urls
  29. import desktop.conf
  30. import logging
  31. import time
  32. from desktop.lib.django_util import TruncatingModel
  33. from desktop.lib.exceptions_renderable import PopupException
  34. import desktop.views as views
  35. import proxy.conf
  36. def setup_test_environment():
  37. """
  38. Sets up mako to signal template rendering.
  39. """
  40. django_mako.render_to_string = django_mako.render_to_string_test
  41. setup_test_environment.__test__ = False
  42. def teardown_test_environment():
  43. """
  44. This method is called by nose_runner when
  45. the tests all finish. This helps track
  46. down when tests aren't cleaning up after
  47. themselves and leaving threads hanging around.
  48. """
  49. import threading
  50. # We should shut down all relevant threads by test completion.
  51. threads = list(threading.enumerate())
  52. try:
  53. import threadframe
  54. import traceback
  55. if len(threads) > 1:
  56. for v in threadframe.dict().values():
  57. traceback.print_stack(v)
  58. finally:
  59. # threadframe is only available in the dev build.
  60. pass
  61. assert 1 == len(threads), threads
  62. django_mako.render_to_string = django_mako.render_to_string_normal
  63. teardown_test_environment.__test__ = False
  64. def test_log_view():
  65. c = make_logged_in_client()
  66. URL = reverse(views.log_view)
  67. LOG = logging.getLogger(__name__)
  68. LOG.warn('une voix m’a réveillé')
  69. # UnicodeDecodeError: 'ascii' codec can't decode byte... should not happen
  70. response = c.get(URL)
  71. assert_equal(200, response.status_code)
  72. c = make_logged_in_client()
  73. URL = reverse(views.log_view)
  74. LOG = logging.getLogger(__name__)
  75. LOG.warn('Got response: PK\x03\x04\n\x00\x00\x08\x00\x00\xad\x0cN?\x00\x00\x00\x00')
  76. # DjangoUnicodeDecodeError: 'utf8' codec can't decode byte 0xad in position 75: invalid start byte... should not happen
  77. response = c.get(URL)
  78. assert_equal(200, response.status_code)
  79. def test_dump_config():
  80. c = make_logged_in_client()
  81. CANARY = "abracadabra"
  82. clear = desktop.conf.HTTP_HOST.set_for_testing(CANARY)
  83. response1 = c.get('/dump_config')
  84. assert_true(CANARY in response1.content)
  85. response2 = c.get('/dump_config', dict(private="true"))
  86. assert_true(CANARY in response2.content)
  87. # There are more private variables...
  88. assert_true(len(response1.content) < len(response2.content))
  89. clear()
  90. CANARY = "(localhost|127\.0\.0\.1):(50030|50070|50060|50075)"
  91. clear = proxy.conf.WHITELIST.set_for_testing(CANARY)
  92. response1 = c.get('/dump_config')
  93. assert_true(CANARY in response1.content)
  94. clear()
  95. # Malformed port per HUE-674
  96. CANARY = "asdfoijaoidfjaosdjffjfjaoojosjfiojdosjoidjfoa"
  97. clear = desktop.conf.HTTP_PORT.set_for_testing(CANARY)
  98. response1 = c.get('/dump_config')
  99. assert_true(CANARY in response1.content, response1.content)
  100. clear()
  101. def test_prefs():
  102. c = make_logged_in_client()
  103. # Get everything
  104. response = c.get('/prefs/')
  105. assert_equal('{}', response.content)
  106. # Set and get
  107. response = c.get('/prefs/foo', dict(set="bar"))
  108. assert_equal('true', response.content)
  109. response = c.get('/prefs/foo')
  110. assert_equal('"bar"', response.content)
  111. # Reset (use post this time)
  112. c.post('/prefs/foo', dict(set="baz"))
  113. response = c.get('/prefs/foo')
  114. assert_equal('"baz"', response.content)
  115. # Check multiple values
  116. c.post('/prefs/elephant', dict(set="room"))
  117. response = c.get('/prefs/')
  118. assert_true("baz" in response.content)
  119. assert_true("room" in response.content)
  120. # Delete everything
  121. c.get('/prefs/elephant', dict(delete=""))
  122. c.get('/prefs/foo', dict(delete=""))
  123. response = c.get('/prefs/')
  124. assert_equal('{}', response.content)
  125. # Check non-existent value
  126. response = c.get('/prefs/doesNotExist')
  127. assert_equal('null', response.content)
  128. def test_status_bar():
  129. """
  130. Subs out the status_bar_views registry with temporary examples.
  131. Tests handling of errors on view functions.
  132. """
  133. backup = views._status_bar_views
  134. views._status_bar_views = []
  135. c = make_logged_in_client()
  136. views.register_status_bar_view(lambda _: HttpResponse("foo", status=200))
  137. views.register_status_bar_view(lambda _: HttpResponse("bar"))
  138. views.register_status_bar_view(lambda _: None)
  139. def f(r):
  140. raise Exception()
  141. views.register_status_bar_view(f)
  142. response = c.get("/status_bar")
  143. assert_equal("foobar", response.content)
  144. views._status_bar_views = backup
  145. def test_paginator():
  146. """
  147. Test that the paginator works with partial list.
  148. """
  149. def assert_page(page, data, start, end):
  150. assert_equal(page.object_list, data)
  151. assert_equal(page.start_index(), start)
  152. assert_equal(page.end_index(), end)
  153. # First page 1-20
  154. obj = range(20)
  155. pgn = Paginator(obj, per_page=20, total=25)
  156. assert_page(pgn.page(1), obj, 1, 20)
  157. # Second page 21-25
  158. obj = range(5)
  159. pgn = Paginator(obj, per_page=20, total=25)
  160. assert_page(pgn.page(2), obj, 21, 25)
  161. # Handle extra data on first page (22 items on a 20-page)
  162. obj = range(22)
  163. pgn = Paginator(obj, per_page=20, total=25)
  164. assert_page(pgn.page(1), range(20), 1, 20)
  165. # Handle extra data on second page (22 items on a 20-page)
  166. obj = range(22)
  167. pgn = Paginator(obj, per_page=20, total=25)
  168. assert_page(pgn.page(2), range(5), 21, 25)
  169. # Handle total < len(obj). Only works for QuerySet.
  170. obj = query.QuerySet()
  171. obj._result_cache = range(10)
  172. pgn = Paginator(obj, per_page=10, total=9)
  173. assert_page(pgn.page(1), range(10), 1, 10)
  174. # Still works with a normal complete list
  175. obj = range(25)
  176. pgn = Paginator(obj, per_page=20)
  177. assert_page(pgn.page(1), range(20), 1, 20)
  178. assert_page(pgn.page(2), range(20, 25), 21, 25)
  179. def test_thread_dump():
  180. c = make_logged_in_client()
  181. response = c.get("/debug/threads")
  182. assert_true("test_thread_dump" in response.content)
  183. def test_truncating_model():
  184. class TinyModel(TruncatingModel):
  185. short_field = CharField(max_length=10)
  186. non_string_field = SmallIntegerField()
  187. a = TinyModel()
  188. a.short_field = 'a' * 9 # One less than it's max length
  189. assert_true(a.short_field == 'a' * 9, 'Short-enough field does not get truncated')
  190. a.short_field = 'a' * 11 # One more than it's max_length
  191. assert_true(a.short_field == 'a' * 10, 'Too-long field gets truncated')
  192. a.non_string_field = 10**10
  193. assert_true(a.non_string_field == 10**10, 'non-string fields are not truncated')
  194. def test_error_handling():
  195. restore_django_debug = desktop.conf.DJANGO_DEBUG_MODE.set_for_testing(False)
  196. restore_500_debug = desktop.conf.HTTP_500_DEBUG_MODE.set_for_testing(False)
  197. exc_msg = "error_raising_view: Test earráid handling"
  198. def error_raising_view(request, *args, **kwargs):
  199. raise Exception(exc_msg)
  200. def popup_exception_view(request, *args, **kwargs):
  201. raise PopupException(exc_msg, title="earráid", detail=exc_msg)
  202. # Add an error view
  203. error_url_pat = patterns('',
  204. url('^500_internal_error$', error_raising_view),
  205. url('^popup_exception$', popup_exception_view))
  206. desktop.urls.urlpatterns.extend(error_url_pat)
  207. try:
  208. def store_exc_info(*args, **kwargs):
  209. pass
  210. # Disable the test client's exception forwarding
  211. c = make_logged_in_client()
  212. c.store_exc_info = store_exc_info
  213. response = c.get('/500_internal_error')
  214. assert_true('500.mako' in response.template)
  215. assert_true('Thank you for your patience' in response.content)
  216. assert_true(exc_msg not in response.content)
  217. # Now test the 500 handler with backtrace
  218. desktop.conf.HTTP_500_DEBUG_MODE.set_for_testing(True)
  219. response = c.get('/500_internal_error')
  220. assert_equal(response.template.name, 'Technical 500 template')
  221. assert_true(exc_msg in response.content)
  222. # PopupException
  223. response = c.get('/popup_exception')
  224. assert_true('popup_error.mako' in response.template)
  225. assert_true(exc_msg in response.content)
  226. finally:
  227. # Restore the world
  228. for i in error_url_pat:
  229. desktop.urls.urlpatterns.remove(i)
  230. restore_django_debug()
  231. restore_500_debug()
  232. def test_404_handling():
  233. view_name = '/the-view-that-is-not-there'
  234. c = make_logged_in_client()
  235. response = c.get(view_name)
  236. assert_true('404.mako' in response.template)
  237. assert_true('Not Found' in response.content)
  238. assert_true(view_name in response.content)
  239. class RecordingHandler(logging.Handler):
  240. def __init__(self, *args, **kwargs):
  241. logging.Handler.__init__(self, *args, **kwargs)
  242. self.records = []
  243. def emit(self, r):
  244. self.records.append(r)
  245. def test_log_event():
  246. c = make_logged_in_client()
  247. root = logging.getLogger("desktop.views.log_frontend_event")
  248. handler = RecordingHandler()
  249. root.addHandler(handler)
  250. c.get("/log_frontend_event?level=info&message=foo")
  251. assert_equal("INFO", handler.records[-1].levelname)
  252. assert_equal("Untrusted log event from user test: foo", handler.records[-1].message)
  253. assert_equal("desktop.views.log_frontend_event", handler.records[-1].name)
  254. c.get("/log_frontend_event?level=error&message=foo2")
  255. assert_equal("ERROR", handler.records[-1].levelname)
  256. assert_equal("Untrusted log event from user test: foo2", handler.records[-1].message)
  257. c.get("/log_frontend_event?message=foo3")
  258. assert_equal("INFO", handler.records[-1].levelname)
  259. assert_equal("Untrusted log event from user test: foo3", handler.records[-1].message)
  260. c.post("/log_frontend_event", {
  261. "message": "01234567" * 1024})
  262. assert_equal("INFO", handler.records[-1].levelname)
  263. assert_equal("Untrusted log event from user test: " + "01234567"*(1024/8),
  264. handler.records[-1].message)
  265. root.removeHandler(handler)
  266. def test_validate_path():
  267. reset = desktop.conf.SSL_PRIVATE_KEY.set_for_testing('/')
  268. assert_equal([], validate_path(desktop.conf.SSL_PRIVATE_KEY, is_dir=True))
  269. reset()
  270. reset = desktop.conf.SSL_PRIVATE_KEY.set_for_testing('/tmm/does_not_exist')
  271. assert_not_equal([], validate_path(desktop.conf.SSL_PRIVATE_KEY, is_dir=True))
  272. reset()
  273. def test_config_check():
  274. reset = (
  275. desktop.conf.SECRET_KEY.set_for_testing(''),
  276. desktop.conf.SSL_CERTIFICATE.set_for_testing('foobar'),
  277. desktop.conf.SSL_PRIVATE_KEY.set_for_testing(''),
  278. desktop.conf.DEFAULT_SITE_ENCODING.set_for_testing('klingon')
  279. )
  280. try:
  281. cli = make_logged_in_client()
  282. resp = cli.get('/debug/check_config')
  283. assert_true('Secret key should be configured' in resp.content)
  284. assert_true('desktop.ssl_certificate' in resp.content)
  285. assert_true('Path does not exist' in resp.content)
  286. assert_true('SSL private key file should be set' in resp.content)
  287. assert_true('klingon' in resp.content)
  288. assert_true('Encoding not supported' in resp.content)
  289. # Alert present in the status bar
  290. resp = cli.get('/status_bar/')
  291. assert_true('Misconfiguration' in resp.content)
  292. finally:
  293. for old_conf in reset:
  294. old_conf()
  295. def test_last_access_time():
  296. c = make_logged_in_client(username="access_test")
  297. c.post('/accounts/login/')
  298. login = desktop.auth.views.get_current_users()
  299. before_access_time = time.time()
  300. response = c.post('/status_bar')
  301. after_access_time = time.time()
  302. access = desktop.auth.views.get_current_users()
  303. user = response.context['user']
  304. login_time = login[user]['time']
  305. access_time = access[user]['time']
  306. # Check that 'last_access_time' is later than login time
  307. assert_true(login_time < access_time)
  308. # Check that 'last_access_time' is in between the timestamps before and after the last access path
  309. assert_true(before_access_time < access_time)
  310. assert_true(access_time < after_access_time)
  311. def test_ui_customizations():
  312. custom_banner = 'test ui customization'
  313. reset = (
  314. desktop.conf.CUSTOM.BANNER_TOP_HTML.set_for_testing(custom_banner),
  315. )
  316. try:
  317. c = make_logged_in_client()
  318. resp = c.get('/debug/check_config')
  319. assert_true(custom_banner in resp.content)
  320. finally:
  321. for old_conf in reset:
  322. old_conf()