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
6from HTMLParser import HTMLParser
7import unittest
8
9from fake_fetchers import ConfigureFakeFetchers
10from github_file_system_provider import GithubFileSystemProvider
11from host_file_system_provider import HostFileSystemProvider
12from patch_servlet import PatchServlet
13from render_servlet import RenderServlet
14from server_instance import ServerInstance
15from servlet import Request
16from test_branch_utility import TestBranchUtility
17from test_util import DisableLogging
18
19
20
21_ALLOWED_HOST = 'https://chrome-apps-doc.appspot.com'
22
23
24def _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
41class _RenderServletDelegate(RenderServlet.Delegate):
42  def CreateServerInstance(self):
43    return ServerInstance.ForLocal()
44
45class _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
56class 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
166if __name__ == '__main__':
167  unittest.main()
168