1 #!/usr/bin/env python 2 # Copyright 2013 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 from HTMLParser import HTMLParser 7 import unittest 8 9 from fake_fetchers import ConfigureFakeFetchers 10 from github_file_system_provider import GithubFileSystemProvider 11 from host_file_system_provider import HostFileSystemProvider 12 from patch_servlet import PatchServlet 13 from render_servlet import RenderServlet 14 from server_instance import ServerInstance 15 from servlet import Request 16 from test_branch_utility import TestBranchUtility 17 from test_util import DisableLogging 18 19 20 21 _ALLOWED_HOST = 'https://chrome-apps-doc.appspot.com' 22 23 24 def _CheckURLsArePatched(content, patch_servlet_path): 25 errors = [] 26 class LinkChecker(HTMLParser): 27 def handle_starttag(self, tag, attrs): 28 if tag != 'a': 29 return 30 tag_description = '<a %s .../>' % ' '.join('%s="%s"' % (key, val) 31 for key, val in attrs) 32 attrs = dict(attrs) 33 if ('href' in attrs and 34 attrs['href'].startswith('/') and 35 not attrs['href'].startswith('/%s/' % patch_servlet_path)): 36 errors.append('%s has an unqualified href' % tag_description) 37 LinkChecker().feed(content) 38 return errors 39 40 41 class _RenderServletDelegate(RenderServlet.Delegate): 42 def CreateServerInstance(self): 43 return ServerInstance.ForLocal() 44 45 class _PatchServletDelegate(RenderServlet.Delegate): 46 def CreateBranchUtility(self, object_store_creator): 47 return TestBranchUtility.CreateWithCannedData() 48 49 def CreateHostFileSystemProvider(self, object_store_creator, **optargs): 50 return HostFileSystemProvider.ForLocal(object_store_creator, **optargs) 51 52 def CreateGithubFileSystemProvider(self, object_store_creator): 53 return GithubFileSystemProvider.ForEmpty() 54 55 56 class PatchServletTest(unittest.TestCase): 57 def setUp(self): 58 ConfigureFakeFetchers() 59 60 def _RenderWithPatch(self, path, issue): 61 path_with_issue = '%s/%s' % (issue, path) 62 return PatchServlet(Request.ForTest(path_with_issue, host=_ALLOWED_HOST), 63 _PatchServletDelegate()).Get() 64 65 def _RenderWithoutPatch(self, path): 66 return RenderServlet(Request.ForTest(path, host=_ALLOWED_HOST), 67 _RenderServletDelegate()).Get() 68 69 def _RenderAndCheck(self, path, issue, expected_equal): 70 '''Renders |path| with |issue| patched in and asserts that the result is 71 the same as |expected_equal| modulo any links that get rewritten to 72 "_patch/issue". 73 ''' 74 patched_response = self._RenderWithPatch(path, issue) 75 unpatched_response = self._RenderWithoutPatch(path) 76 for header in ('Cache-Control', 'ETag'): 77 patched_response.headers.pop(header, None) 78 unpatched_response.headers.pop(header, None) 79 unpatched_content = unpatched_response.content.ToString() 80 81 # Check that all links in the patched content are qualified with 82 # the patch URL, then strip them out for checking (in)equality. 83 patched_content = patched_response.content.ToString() 84 patch_servlet_path = '_patch/%s' % issue 85 errors = _CheckURLsArePatched(patched_content, patch_servlet_path) 86 self.assertFalse(errors, 87 '%s\nFound errors:\n * %s' % (patched_content, '\n * '.join(errors))) 88 patched_content = patched_content.replace('/%s' % patch_servlet_path, '') 89 90 self.assertEqual(patched_response.status, unpatched_response.status) 91 self.assertEqual(patched_response.headers, unpatched_response.headers) 92 if expected_equal: 93 self.assertEqual(patched_content, unpatched_content) 94 else: 95 self.assertNotEqual(patched_content, unpatched_content) 96 97 def _RenderAndAssertEqual(self, path, issue): 98 self._RenderAndCheck(path, issue, True) 99 100 def _RenderAndAssertNotEqual(self, path, issue): 101 self._RenderAndCheck(path, issue, False) 102 103 @DisableLogging('warning') 104 def _AssertNotFound(self, path, issue): 105 response = self._RenderWithPatch(path, issue) 106 self.assertEqual(response.status, 404, 107 'Path %s with issue %s should have been removed for %s.' % ( 108 path, issue, response)) 109 110 def _AssertOk(self, path, issue): 111 response = self._RenderWithPatch(path, issue) 112 self.assertEqual(response.status, 200, 113 'Failed to render path %s with issue %s.' % (path, issue)) 114 self.assertTrue(len(response.content.ToString()) > 0, 115 'Rendered result for path %s with issue %s should not be empty.' % 116 (path, issue)) 117 118 def _AssertRedirect(self, path, issue, redirect_path): 119 response = self._RenderWithPatch(path, issue) 120 self.assertEqual(302, response.status) 121 self.assertEqual('/_patch/%s/%s' % (issue, redirect_path), 122 response.headers['Location']) 123 124 def testRender(self): 125 # '_patch' is not included in paths below because it's stripped by Handler. 126 issue = '14096030' 127 128 # TODO(kalman): Test with chrome_sidenav.json once the sidenav logic has 129 # stabilised. 130 131 # extensions/runtime.html is removed in the patch, should redirect to the 132 # apps version. 133 self._AssertRedirect('extensions/runtime', issue, 'apps/runtime') 134 135 # apps/runtime.html is not removed. 136 self._RenderAndAssertEqual('apps/runtime', issue) 137 138 # test_foo.html is added in the patch. 139 self._AssertOk('extensions/test_foo', issue) 140 141 # Invalid issue number results in a 404. 142 self._AssertNotFound('extensions/index', '11111') 143 144 def testXssRedirect(self): 145 def is_redirect(from_host, from_path, to_url): 146 response = PatchServlet(Request.ForTest(from_path, host=from_host), 147 _PatchServletDelegate()).Get() 148 redirect_url, _ = response.GetRedirect() 149 if redirect_url is None: 150 return (False, '%s/%s did not cause a redirect' % ( 151 from_host, from_path)) 152 if redirect_url != to_url: 153 return (False, '%s/%s redirected to %s not %s' % ( 154 from_host, from_path, redirect_url, to_url)) 155 return (True, '%s/%s redirected to %s' % ( 156 from_host, from_path, redirect_url)) 157 self.assertTrue(*is_redirect('http://developer.chrome.com', '12345', 158 '%s/_patch/12345' % _ALLOWED_HOST)) 159 self.assertTrue(*is_redirect('http://developers.google.com', '12345', 160 '%s/_patch/12345' % _ALLOWED_HOST)) 161 self.assertFalse(*is_redirect('http://chrome-apps-doc.appspot.com', '12345', 162 None)) 163 self.assertFalse(*is_redirect('http://some-other-app.appspot.com', '12345', 164 None)) 165 166 if __name__ == '__main__': 167 unittest.main() 168