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.
6 from HTMLParser import HTMLParser
9 from empty_dir_file_system import EmptyDirFileSystem
10 from fake_fetchers import ConfigureFakeFetchers
11 from github_file_system_provider import GithubFileSystemProvider
12 from host_file_system_provider import HostFileSystemProvider
13 from patch_servlet import PatchServlet
14 from render_servlet import RenderServlet
15 from server_instance import ServerInstance
16 from servlet import Request
17 from test_branch_utility import TestBranchUtility
18 from test_util import DisableLogging
22 _ALLOWED_HOST = 'https://chrome-apps-doc.appspot.com'
25 def _CheckURLsArePatched(content, patch_servlet_path):
27 class LinkChecker(HTMLParser):
28 def handle_starttag(self, tag, attrs):
31 tag_description = '<a %s .../>' % ' '.join('%s="%s"' % (key, val)
32 for key, val in attrs)
34 if ('href' in attrs and
35 attrs['href'].startswith('/') and
36 not attrs['href'].startswith('/%s/' % patch_servlet_path)):
37 errors.append('%s has an unqualified href' % tag_description)
38 LinkChecker().feed(content)
42 class _RenderServletDelegate(RenderServlet.Delegate):
43 def CreateServerInstance(self):
44 return ServerInstance.ForLocal()
46 class _PatchServletDelegate(RenderServlet.Delegate):
47 def CreateBranchUtility(self, object_store_creator):
48 return TestBranchUtility.CreateWithCannedData()
50 def CreateHostFileSystemProvider(self, object_store_creator, **optargs):
51 return HostFileSystemProvider.ForLocal(object_store_creator, **optargs)
53 def CreateGithubFileSystemProvider(self, object_store_creator):
54 return GithubFileSystemProvider.ForEmpty()
57 class PatchServletTest(unittest.TestCase):
59 ConfigureFakeFetchers()
61 def _RenderWithPatch(self, path, issue):
62 path_with_issue = '%s/%s' % (issue, path)
63 return PatchServlet(Request.ForTest(path_with_issue, host=_ALLOWED_HOST),
64 _PatchServletDelegate()).Get()
66 def _RenderWithoutPatch(self, path):
67 return RenderServlet(Request.ForTest(path, host=_ALLOWED_HOST),
68 _RenderServletDelegate()).Get()
70 def _RenderAndCheck(self, path, issue, expected_equal):
71 '''Renders |path| with |issue| patched in and asserts that the result is
72 the same as |expected_equal| modulo any links that get rewritten to
75 patched_response = self._RenderWithPatch(path, issue)
76 unpatched_response = self._RenderWithoutPatch(path)
77 patched_response.headers.pop('cache-control', None)
78 unpatched_response.headers.pop('cache-control', None)
79 unpatched_content = unpatched_response.content.ToString()
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, '')
90 self.assertEqual(patched_response.status, unpatched_response.status)
91 self.assertEqual(patched_response.headers, unpatched_response.headers)
93 self.assertEqual(patched_content, unpatched_content)
95 self.assertNotEqual(patched_content, unpatched_content)
97 def _RenderAndAssertEqual(self, path, issue):
98 self._RenderAndCheck(path, issue, True)
100 def _RenderAndAssertNotEqual(self, path, issue):
101 self._RenderAndCheck(path, issue, False)
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))
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.' %
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'])
124 def testRender(self):
125 # '_patch' is not included in paths below because it's stripped by Handler.
128 # extensions_sidenav.json is modified in the patch.
129 self._RenderAndAssertNotEqual('extensions/index.html', issue)
131 # apps_sidenav.json is not patched.
132 self._RenderAndAssertEqual('apps/about_apps.html', issue)
134 # extensions/runtime.html is removed in the patch, should redirect to the
136 self._AssertRedirect('extensions/runtime.html', issue,
139 # apps/runtime.html is not removed.
140 self._RenderAndAssertEqual('apps/runtime.html', issue)
142 # test_foo.html is added in the patch.
143 self._AssertOk('extensions/test_foo.html', issue)
145 # Invalid issue number results in a 404.
146 self._AssertNotFound('extensions/index.html', '11111')
148 def testXssRedirect(self):
149 def is_redirect(from_host, from_path, to_url):
150 response = PatchServlet(Request.ForTest(from_path, host=from_host),
151 _PatchServletDelegate()).Get()
152 redirect_url, _ = response.GetRedirect()
153 if redirect_url is None:
154 return (False, '%s/%s did not cause a redirect' % (
155 from_host, from_path))
156 if redirect_url != to_url:
157 return (False, '%s/%s redirected to %s not %s' % (
158 from_host, from_path, redirect_url, to_url))
159 return (True, '%s/%s redirected to %s' % (
160 from_host, from_path, redirect_url))
161 self.assertTrue(*is_redirect('http://developer.chrome.com', '12345',
162 '%s/_patch/12345' % _ALLOWED_HOST))
163 self.assertTrue(*is_redirect('http://developers.google.com', '12345',
164 '%s/_patch/12345' % _ALLOWED_HOST))
165 self.assertFalse(*is_redirect('http://chrome-apps-doc.appspot.com', '12345',
167 self.assertFalse(*is_redirect('http://some-other-app.appspot.com', '12345',
170 if __name__ == '__main__':