from unittest import TestCase as RegularTestCase from unittest.mock import patch from django.utils.safestring import SafeString from django.utils.html import conditional_escape from bugsink.pygments_extensions import choose_lexer_for_pattern, get_all_lexers from events.utils import IncompleteList, IncompleteDict from .templatetags.issues import ( _pygmentize_lines as actual_pygmentize_lines, format_var, pygmentize, timestamp_with_millis) class TestPygmentizeLineLineCountHandling(RegularTestCase): # The focus of these tests is `len(input) == len(output)`, which is hard in the presence of emptyness. # # For failure we depend on the asserts inside the function, simply calling the function and the assert not blowing # up is what we're proving here. def setUp(self): super().setUp() patcher = patch("theme.templatetags.issues.capture_stacktrace") self.capture_mock = patcher.start() self.addCleanup(patcher.stop) def _pygmentize_lines(self, lines): # since we exclusively care about line-counts, we just pick something for filename and platform here. result = actual_pygmentize_lines(lines, filename="a.py", platform="python") self.capture_mock.assert_not_called() return result def test_pygmentize_lines_empty(self): self._pygmentize_lines([]) def test_pygmentize_lines_single_empty_line(self): self._pygmentize_lines([""]) def test_pygmentize_lines_single_space(self): self._pygmentize_lines([" "]) def test_pygmentize_lines_single_line(self): self._pygmentize_lines(["print('hello world')"]) def test_pygmentize_lines_leading_and_trailing_emptyness_0_1(self): self._pygmentize_lines(["print('hello world')", ""]) def test_pygmentize_lines_leading_and_trailing_emptyness_0_2(self): self._pygmentize_lines(["print('hello world')", "", ""]) def test_pygmentize_lines_leading_and_trailing_emptyness_2_0(self): self._pygmentize_lines(["", "", "print('hello world')"]) def test_pygmentize_lines_leading_and_trailing_emptyness_1_1(self): self._pygmentize_lines(["", "print('hello world')", ""]) def test_pygmentize_lines_leading_and_trailing_emptyness_2_1(self): self._pygmentize_lines(["", "", "print('hello world')", ""]) def test_pygmentize_lines_leading_and_trailing_emptyness_1_2(self): self._pygmentize_lines(["", "print('hello world')", "", ""]) def test_pygmentize_lines_leading_and_trailing_emptyness_2_2(self): self._pygmentize_lines(["", "", "print('hello world')", "", ""]) def test_pygmentize_lines_newlines_in_the_middle(self): self._pygmentize_lines(["print('hello world')", "", "", "print('goodbye')"]) def test_pygmentize_lines_non_python(self): # not actually python self._pygmentize_lines([""]) def test_pygmentize_lines_newline_in_code(self): self._pygmentize_lines(["print('hello world')\n"]) def test_pygmentize_lines_newline_on_otherwise_empty_line(self): self._pygmentize_lines(["\n", "\n", "\n"]) def test_pygmentize_lines_ruby_regression(self): # code taken from: # https://github.com/rails/rails/blob/0f969a989c87/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb code = """ # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: exec_query(<<-end_sql, 'SCHEMA').rows SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod FROM pg_attribute a LEFT JOIN pg_attrdef d""" code_as_list = code.splitlines() actual_pygmentize_lines(code_as_list, filename="postgresql_adapter.rb", platform="ruby") self.capture_mock.assert_called() # https://github.com/pygments/pygments/issues/2998 class TestChooseLexerForPattern(RegularTestCase): def test_choose_lexer_for_pattern(self): # simple 'does it not crash' test: for pattern, lexers in get_all_lexers()._list: choose_lexer_for_pattern(pattern, lexers, "", "", "python") class TestFormatVar(RegularTestCase): def _format_var(self, var): # small helper for readable tests return format_var(var).replace("'", "'") def test_format_var_none(self): # This is how we've actually observed None values in the SDKs, so we should also handle it self.assertEqual( "None", self._format_var("None"), ) # I _think_ SDKs generally don't send null (None) as a value, but if/when they do we should handle it # gracefully. See #119 self.assertEqual( "None", self._format_var(None), ) def test_format_var_nested(self): var = { "a": 1, "b": [2, 3], "c": {"d": 4}, "d": [], "e": {}, "f": "None", "g": "", } self.assertEqual( "{'a': 1, 'b': [2, 3], 'c': {'d': 4}, 'd': [], 'e': {}, 'f': None, 'g': <python obj>}", self._format_var(var), ) def test_format_var_nested_escaping(self): # like format_nested, but with the focus on "does escaping happen correctly?" var = { "hacker": ["'], 'context_line': '', 'post_context': [''], }, platform='python', ) for line in out['pre_context'] + [out['context_line']] + out['post_context']: self.assertIsInstance(line, SafeString) # we just check for the non-existance of here because asserting against "whatever # pygmentize does" is not very useful, as it may change in the future. self.assertFalse("" in line) class TimestampWithMillisTagTest(RegularTestCase): def test_float_input_produces_expected_safe_string(self): ts = 1620130245.1234 self.assertEqual( '4 May 12:10:45.123', timestamp_with_millis(ts)) self.assertTrue(isinstance(timestamp_with_millis(ts), SafeString)) def test_timestamp_with_milis_is_not_a_target_for_html_injection(self): # even though the string input is returned as-is for this case, the tag will not mark it as safe in the process. ts = "" self.assertEqual( '<script>alert('hello');</script>', conditional_escape(timestamp_with_millis(ts))) self.assertFalse(isinstance(timestamp_with_millis(ts), SafeString))