|
|
@@ -1,17 +1,16 @@ |
|
|
from unittest.mock import patch |
|
|
from unittest.mock import patch, Mock |
|
|
|
|
|
import os |
|
|
import io |
|
|
import json |
|
|
|
|
|
from testfixtures import LogCapture |
|
|
import jsonschema |
|
|
|
|
|
from django.test import override_settings |
|
|
from django.core.urlresolvers import reverse |
|
|
from django.db import OperationalError |
|
|
|
|
|
from constance.test import override_config |
|
|
|
|
|
from ..utils import TestCase |
|
|
from .logging import JsonLogFormatter |
|
|
from .jinja import _hash_content, waffleintegrity, TestPilotExtension |
|
|
|
|
|
import logging |
|
|
logger = logging.getLogger(__name__) |
|
|
@@ -90,154 +89,76 @@ def test_schema(self): |
|
|
assert set(['name', 'description', 'repository']).issubset(data.keys()) |
|
|
|
|
|
|
|
|
class TestJsonLogFormatter(TestCase): |
|
|
class _MockJinjaEnvironment(object): |
|
|
def __init__(self): |
|
|
self.globals = {} |
|
|
|
|
|
|
|
|
MockStorageInstance = Mock() |
|
|
MockStorageInstance.path = Mock(return_value='foo') |
|
|
MockStorageInstance.hashed_files = {"foo": "bar"} |
|
|
MockStorage = Mock(return_value=MockStorageInstance) |
|
|
|
|
|
|
|
|
class TestPilotJinjaExtensionTests(TestCase): |
|
|
|
|
|
def setUp(self): |
|
|
self.handler = LogCapture() |
|
|
self.logger_name = "TestingTestPilot" |
|
|
self.formatter = JsonLogFormatter(logger_name=self.logger_name) |
|
|
|
|
|
def tearDown(self): |
|
|
self.handler.uninstall() |
|
|
|
|
|
def _fetchLastLog(self): |
|
|
self.assertEquals(len(self.handler.records), 1) |
|
|
details = json.loads(self.formatter.format(self.handler.records[0])) |
|
|
jsonschema.validate(details, JSON_LOGGING_SCHEMA) |
|
|
return details |
|
|
|
|
|
def test_basic_operation(self): |
|
|
"""Ensure log formatter contains all the expected fields and values""" |
|
|
message_text = "simple test" |
|
|
logging.debug(message_text) |
|
|
details = self._fetchLastLog() |
|
|
|
|
|
expected_present = ["Timestamp", "Hostname"] |
|
|
for key in expected_present: |
|
|
self.assertTrue(key in details) |
|
|
|
|
|
expected_meta = { |
|
|
"Severity": 7, |
|
|
"Type": "root", |
|
|
"Pid": os.getpid(), |
|
|
"Logger": self.logger_name, |
|
|
"EnvVersion": self.formatter.LOGGING_FORMAT_VERSION |
|
|
} |
|
|
for key, value in expected_meta.items(): |
|
|
self.assertEquals(value, details[key]) |
|
|
|
|
|
self.assertEquals(details['Fields']['message'], message_text) |
|
|
|
|
|
def test_custom_paramters(self): |
|
|
"""Ensure log formatter can handle custom parameters""" |
|
|
logger = logging.getLogger("mozsvc.test.test_logging") |
|
|
logger.warn("custom test %s", "one", extra={"more": "stuff"}) |
|
|
details = self._fetchLastLog() |
|
|
|
|
|
self.assertEquals(details["Type"], "mozsvc.test.test_logging") |
|
|
self.assertEquals(details["Severity"], 4) |
|
|
|
|
|
fields = details['Fields'] |
|
|
self.assertEquals(fields["message"], "custom test one") |
|
|
self.assertEquals(fields["more"], "stuff") |
|
|
|
|
|
def test_logging_error_tracebacks(self): |
|
|
"""Ensure log formatter includes exception traceback information""" |
|
|
try: |
|
|
raise ValueError("\n") |
|
|
except Exception: |
|
|
logging.exception("there was an error") |
|
|
details = self._fetchLastLog() |
|
|
|
|
|
expected_meta = { |
|
|
"Severity": 3, |
|
|
} |
|
|
for key, value in expected_meta.items(): |
|
|
self.assertEquals(value, details[key]) |
|
|
|
|
|
fields = details['Fields'] |
|
|
expected_fields = { |
|
|
'message': 'there was an error', |
|
|
'error': "ValueError('\\n',)" |
|
|
} |
|
|
for key, value in expected_fields.items(): |
|
|
self.assertEquals(value, fields[key]) |
|
|
|
|
|
self.assertTrue(fields['traceback'].startswith('Uncaught exception:')) |
|
|
self.assertTrue("<class 'ValueError'>" in fields['traceback']) |
|
|
|
|
|
|
|
|
# https://mana.mozilla.org/wiki/pages/viewpage.action?pageId=42895640 |
|
|
JSON_LOGGING_SCHEMA = json.loads(""" |
|
|
{ |
|
|
"type":"object", |
|
|
"required":["Timestamp"], |
|
|
"properties":{ |
|
|
"Timestamp":{ |
|
|
"type":"integer", |
|
|
"minimum":0 |
|
|
}, |
|
|
"Type":{ |
|
|
"type":"string" |
|
|
}, |
|
|
"Logger":{ |
|
|
"type":"string" |
|
|
}, |
|
|
"Hostname":{ |
|
|
"type":"string", |
|
|
"format":"hostname" |
|
|
}, |
|
|
"EnvVersion":{ |
|
|
"type":"string", |
|
|
"pattern":"^\\d+(?:\\.\\d+){0,2}$" |
|
|
}, |
|
|
"Severity":{ |
|
|
"type":"integer", |
|
|
"minimum":0, |
|
|
"maximum":7 |
|
|
}, |
|
|
"Pid":{ |
|
|
"type":"integer", |
|
|
"minimum":0 |
|
|
}, |
|
|
"Fields":{ |
|
|
"type":"object", |
|
|
"minProperties":1, |
|
|
"additionalProperties":{ |
|
|
"anyOf": [ |
|
|
{ "$ref": "#/definitions/field_value"}, |
|
|
{ "$ref": "#/definitions/field_array"}, |
|
|
{ "$ref": "#/definitions/field_object"} |
|
|
] |
|
|
} |
|
|
} |
|
|
}, |
|
|
"definitions":{ |
|
|
"field_value":{ |
|
|
"type":["string", "number", "boolean"] |
|
|
}, |
|
|
"field_array":{ |
|
|
"type":"array", |
|
|
"minItems": 1, |
|
|
"oneOf": [ |
|
|
{"items": {"type":"string"}}, |
|
|
{"items": {"type":"number"}}, |
|
|
{"items": {"type":"boolean"}} |
|
|
] |
|
|
}, |
|
|
"field_object":{ |
|
|
"type":"object", |
|
|
"required":["value"], |
|
|
"properties":{ |
|
|
"value":{ |
|
|
"oneOf": [ |
|
|
{ "$ref": "#/definitions/field_value" }, |
|
|
{ "$ref": "#/definitions/field_array" } |
|
|
] |
|
|
}, |
|
|
"representation":{"type":"string"} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
""".replace("\\", "\\\\")) # HACK: Fix escaping for easy copy/paste |
|
|
self.environment = _MockJinjaEnvironment() |
|
|
self.extension = TestPilotExtension(self.environment) |
|
|
|
|
|
def test_hash_content(self): |
|
|
content = """ |
|
|
Beard irony cold-pressed, venmo chicharrones PBR&B banh mi |
|
|
meditation. Forage street art meh artisan, tattooed |
|
|
gochujang pinterest fixie skateboard kombucha crucifix viral. |
|
|
""" |
|
|
self.assertEqual( |
|
|
_hash_content(content.encode()), |
|
|
'sha384-1hLxRtQHxIHoyvc9LOFMWEOGa2gsPIK+4e5++etpYBwkHjxWe9MQOXp/9e4zYqHm' |
|
|
) |
|
|
|
|
|
@override_settings(DEBUG=True) |
|
|
@patch('django.contrib.staticfiles.finders') |
|
|
@patch('builtins.open') |
|
|
def test_staticintegrity_debug(self, mock_open, mock_finders): |
|
|
content = 'debug file' |
|
|
expected_hash = _hash_content(content.encode()) |
|
|
mock_finders.find = Mock(return_value='foo') |
|
|
mock_open.return_value = io.BytesIO(content.encode('utf-8')) |
|
|
result = self.environment.globals['staticintegrity'](None, 'foo') |
|
|
self.assertEqual(result, expected_hash) |
|
|
|
|
|
@override_settings(DEBUG=False) |
|
|
@override_settings(STATICFILES_STORAGE='testpilot.base.tests.MockStorage') |
|
|
@patch('builtins.open') |
|
|
def test_staticintegrity_prod(self, mock_open): |
|
|
content = 'prod file' |
|
|
expected_hash = _hash_content(content.encode()) |
|
|
mock_open.return_value = io.BytesIO(content.encode('utf-8')) |
|
|
result = self.environment.globals['staticintegrity'](None, 'foo') |
|
|
MockStorageInstance.path.assert_called_with('bar') |
|
|
self.assertEqual(result, expected_hash) |
|
|
|
|
|
def test_urlintegrity(self): |
|
|
expected_url = 'http://example.com' |
|
|
expected_hash = '8675309' |
|
|
with self.settings(URL_INTEGRITY_HASHES={expected_url: expected_hash}): |
|
|
result = self.environment.globals['urlintegrity'](None, expected_url) |
|
|
self.assertEqual(result, expected_hash) |
|
|
|
|
|
def test_urlintegrity_override(self): |
|
|
expected_url = 'http://example.com' |
|
|
expected_hash = '8675309' |
|
|
overrides = json.dumps({expected_url: expected_hash}) |
|
|
with self.settings(URL_INTEGRITY_HASHES={expected_url: 'override me'}): |
|
|
with override_config(URL_INTEGRITY_HASHES_OVERRIDES=overrides): |
|
|
result = self.environment.globals['urlintegrity'](None, expected_url) |
|
|
self.assertEqual(result, expected_hash) |
|
|
|
|
|
@patch('waffle.views._generate_waffle_js') |
|
|
def test_waffleintegrity(self, mock_generate): |
|
|
expected_content = 'foobarbaz waffle' |
|
|
expected_hash = _hash_content(expected_content.encode()) |
|
|
mock_generate.return_value = expected_content |
|
|
result = waffleintegrity(None, None) |
|
|
self.assertEqual(result, expected_hash) |