blob: ef52f1ec984279d6970f50b53ac9f2e430e3bf92 [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd
"""Unit tests for the monorailrequest module."""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
import endpoints
import mock
import re
import unittest
try:
from mox3 import mox
except ImportError:
import mox
import six
from google.appengine.api import oauth
from google.appengine.api import users
import webapp2
from framework import exceptions
from framework import monorailrequest
from framework import permissions
from proto import project_pb2
from proto import tracker_pb2
from services import service_manager
from testing import fake
from testing import testing_helpers
from tracker import tracker_constants
class HostportReTest(unittest.TestCase):
def testGood(self):
test_data = [
'localhost:8080',
'app.appspot.com',
'bugs-staging.chromium.org',
'vers10n-h3x-dot-app-id.appspot.com',
]
for hostport in test_data:
self.assertTrue(monorailrequest._HOSTPORT_RE.match(hostport),
msg='Incorrectly rejected %r' % hostport)
def testBad(self):
test_data = [
'',
' ',
'\t',
'\n',
'\'',
'"',
'version"cruft-dot-app-id.appspot.com',
'\nother header',
'version&cruft-dot-app-id.appspot.com',
]
for hostport in test_data:
self.assertFalse(monorailrequest._HOSTPORT_RE.match(hostport),
msg='Incorrectly accepted %r' % hostport)
class MonorailApiRequestUnitTest(unittest.TestCase):
def setUp(self):
self.cnxn = 'fake cnxn'
self.services = service_manager.Services(
config=fake.ConfigService(),
issue=fake.IssueService(),
project=fake.ProjectService(),
user=fake.UserService(),
usergroup=fake.UserGroupService())
self.project = self.services.project.TestAddProject(
'proj', project_id=789)
self.services.user.TestAddUser('requester@example.com', 111)
self.issue = fake.MakeTestIssue(
789, 1, 'sum', 'New', 111)
self.services.issue.TestAddIssue(self.issue)
self.patcher_1 = mock.patch('endpoints.get_current_user')
self.mock_endpoints_gcu = self.patcher_1.start()
self.mock_endpoints_gcu.return_value = None
self.patcher_2 = mock.patch('google.appengine.api.oauth.get_current_user')
self.mock_oauth_gcu = self.patcher_2.start()
self.mock_oauth_gcu.return_value = testing_helpers.Blank(
email=lambda: 'requester@example.com')
def tearDown(self):
mock.patch.stopall()
def testInit_NoProjectIssueOrViewedUser(self):
request = testing_helpers.Blank()
mar = monorailrequest.MonorailApiRequest(
request, self.services, cnxn=self.cnxn)
self.assertIsNone(mar.project)
self.assertIsNone(mar.issue)
def testInit_WithProject(self):
request = testing_helpers.Blank(projectId='proj')
mar = monorailrequest.MonorailApiRequest(
request, self.services, cnxn=self.cnxn)
self.assertEqual(self.project, mar.project)
self.assertIsNone(mar.issue)
def testInit_WithProjectAndIssue(self):
request = testing_helpers.Blank(
projectId='proj', issueId=1)
mar = monorailrequest.MonorailApiRequest(
request, self.services, cnxn=self.cnxn)
self.assertEqual(self.project, mar.project)
self.assertEqual(self.issue, mar.issue)
def testGetParam_Normal(self):
request = testing_helpers.Blank(q='owner:me')
mar = monorailrequest.MonorailApiRequest(
request, self.services, cnxn=self.cnxn)
self.assertEqual(None, mar.GetParam('unknown'))
self.assertEqual(100, mar.GetParam('num'))
self.assertEqual('owner:me', mar.GetParam('q'))
request = testing_helpers.Blank(q='owner:me', maxResults=200)
mar = monorailrequest.MonorailApiRequest(
request, self.services, cnxn=self.cnxn)
self.assertEqual(200, mar.GetParam('num'))
class MonorailRequestUnitTest(unittest.TestCase):
def setUp(self):
self.services = service_manager.Services(
project=fake.ProjectService(),
user=fake.UserService(),
usergroup=fake.UserGroupService(),
features=fake.FeaturesService())
self.project = self.services.project.TestAddProject('proj')
self.hotlist = self.services.features.TestAddHotlist(
'TestHotlist', owner_ids=[111])
self.services.user.TestAddUser('jrobbins@example.com', 111)
self.mox = mox.Mox()
self.mox.StubOutWithMock(users, 'get_current_user')
users.get_current_user().AndReturn(None)
self.mox.ReplayAll()
def tearDown(self):
self.mox.UnsetStubs()
def testGetIntParam_ConvertsQueryParamToInt(self):
notice_id = 12345
mr = testing_helpers.MakeMonorailRequest(
path='/foo?notice=%s' % notice_id)
value = mr.GetIntParam('notice')
self.assertTrue(isinstance(value, int))
self.assertEqual(notice_id, value)
def testGetIntParam_ConvertsQueryParamToLong(self):
notice_id = 12345678901234567890
mr = testing_helpers.MakeMonorailRequest(
path='/foo?notice=%s' % notice_id)
value = mr.GetIntParam('notice')
self.assertTrue(isinstance(value, six.integer_types))
self.assertEqual(notice_id, value)
def testGetIntListParam_NoParam(self):
mr = monorailrequest.MonorailRequest(self.services)
mr.ParseRequest(webapp2.Request.blank('servlet'), self.services)
self.assertEqual(mr.GetIntListParam('ids'), None)
self.assertEqual(mr.GetIntListParam('ids', default_value=['test']),
['test'])
def testGetIntListParam_OneValue(self):
mr = monorailrequest.MonorailRequest(self.services)
mr.ParseRequest(webapp2.Request.blank('servlet?ids=11'), self.services)
self.assertEqual(mr.GetIntListParam('ids'), [11])
self.assertEqual(mr.GetIntListParam('ids', default_value=['test']),
[11])
def testGetIntListParam_MultiValue(self):
mr = monorailrequest.MonorailRequest(self.services)
mr.ParseRequest(
webapp2.Request.blank('servlet?ids=21,22,23'), self.services)
self.assertEqual(mr.GetIntListParam('ids'), [21, 22, 23])
self.assertEqual(mr.GetIntListParam('ids', default_value=['test']),
[21, 22, 23])
def testGetIntListParam_BogusValue(self):
mr = monorailrequest.MonorailRequest(self.services)
with self.assertRaises(exceptions.InputException):
mr.ParseRequest(
webapp2.Request.blank('servlet?ids=not_an_int'), self.services)
def testGetIntListParam_Malformed(self):
mr = monorailrequest.MonorailRequest(self.services)
with self.assertRaises(exceptions.InputException):
mr.ParseRequest(
webapp2.Request.blank('servlet?ids=31,32,,'), self.services)
def testDefaultValuesNoUrl(self):
"""If request has no param, default param values should be used."""
mr = monorailrequest.MonorailRequest(self.services)
mr.ParseRequest(webapp2.Request.blank('servlet'), self.services)
self.assertEqual(mr.GetParam('r', 3), 3)
self.assertEqual(mr.GetIntParam('r', 3), 3)
self.assertEqual(mr.GetPositiveIntParam('r', 3), 3)
self.assertEqual(mr.GetIntListParam('r', [3, 4]), [3, 4])
def _MRWithMockRequest(
self, path, headers=None, *mr_args, **mr_kwargs):
request = webapp2.Request.blank(path, headers=headers)
mr = monorailrequest.MonorailRequest(self.services, *mr_args, **mr_kwargs)
mr.ParseRequest(request, self.services)
return mr
def testParseQueryParameters(self):
mr = self._MRWithMockRequest(
'/p/proj/issues/list?q=foo+OR+bar&num=50')
self.assertEqual('foo OR bar', mr.query)
self.assertEqual(50, mr.num)
def testParseQueryParameters_ModeMissing(self):
mr = self._MRWithMockRequest(
'/p/proj/issues/list?q=foo+OR+bar&num=50')
self.assertEqual('list', mr.mode)
def testParseQueryParameters_ModeList(self):
mr = self._MRWithMockRequest(
'/p/proj/issues/list?q=foo+OR+bar&num=50&mode=')
self.assertEqual('list', mr.mode)
def testParseQueryParameters_ModeGrid(self):
mr = self._MRWithMockRequest(
'/p/proj/issues/list?q=foo+OR+bar&num=50&mode=grid')
self.assertEqual('grid', mr.mode)
def testParseQueryParameters_ModeChart(self):
mr = self._MRWithMockRequest(
'/p/proj/issues/list?q=foo+OR+bar&num=50&mode=chart')
self.assertEqual('chart', mr.mode)
def testParseRequest_Scheme(self):
mr = self._MRWithMockRequest('/p/proj/')
self.assertEqual('http', mr.request.scheme)
def testParseRequest_HostportAndCurrentPageURL(self):
mr = self._MRWithMockRequest('/p/proj/', headers={
'Host': 'example.com',
'Cookie': 'asdf',
})
self.assertEqual('http', mr.request.scheme)
self.assertEqual('example.com', mr.request.host)
self.assertEqual('http://example.com/p/proj/', mr.current_page_url)
def testParseRequest_ProjectFound(self):
mr = self._MRWithMockRequest('/p/proj/')
self.assertEqual(mr.project, self.project)
def testParseRequest_ProjectNotFound(self):
with self.assertRaises(exceptions.NoSuchProjectException):
self._MRWithMockRequest('/p/no-such-proj/')
def testViewedUser_WithEmail(self):
mr = self._MRWithMockRequest('/u/jrobbins@example.com/')
self.assertEqual('jrobbins@example.com', mr.viewed_username)
self.assertEqual(111, mr.viewed_user_auth.user_id)
self.assertEqual(
self.services.user.GetUser('fake cnxn', 111),
mr.viewed_user_auth.user_pb)
def testViewedUser_WithUserID(self):
mr = self._MRWithMockRequest('/u/111/')
self.assertEqual('jrobbins@example.com', mr.viewed_username)
self.assertEqual(111, mr.viewed_user_auth.user_id)
self.assertEqual(
self.services.user.GetUser('fake cnxn', 111),
mr.viewed_user_auth.user_pb)
def testViewedUser_NoSuchEmail(self):
with self.assertRaises(webapp2.HTTPException) as cm:
self._MRWithMockRequest('/u/unknownuser@example.com/')
self.assertEqual(404, cm.exception.code)
def testViewedUser_NoSuchUserID(self):
with self.assertRaises(exceptions.NoSuchUserException):
self._MRWithMockRequest('/u/234521111/')
def testGetParam(self):
mr = testing_helpers.MakeMonorailRequest(
path='/foo?syn=error!&a=a&empty=',
params=dict(over1='over_value1', over2='over_value2'))
# test tampering
self.assertRaises(exceptions.InputException, mr.GetParam, 'a',
antitamper_re=re.compile(r'^$'))
self.assertRaises(exceptions.InputException, mr.GetParam,
'undefined', default_value='default',
antitamper_re=re.compile(r'^$'))
# test empty value
self.assertEqual('', mr.GetParam(
'empty', default_value='default', antitamper_re=re.compile(r'^$')))
# test default
self.assertEqual('default', mr.GetParam(
'undefined', default_value='default'))
def testComputeColSpec(self):
# No config passed, and nothing in URL
mr = testing_helpers.MakeMonorailRequest(
path='/p/proj/issues/detail?id=123')
mr.ComputeColSpec(None)
self.assertEqual(tracker_constants.DEFAULT_COL_SPEC, mr.col_spec)
# No config passed, but set in URL
mr = testing_helpers.MakeMonorailRequest(
path='/p/proj/issues/detail?id=123&colspec=a b C')
mr.ComputeColSpec(None)
self.assertEqual('a b C', mr.col_spec)
config = tracker_pb2.ProjectIssueConfig()
# No default in the config, and nothing in URL
mr = testing_helpers.MakeMonorailRequest(
path='/p/proj/issues/detail?id=123')
mr.ComputeColSpec(config)
self.assertEqual(tracker_constants.DEFAULT_COL_SPEC, mr.col_spec)
# No default in the config, but set in URL
mr = testing_helpers.MakeMonorailRequest(
path='/p/proj/issues/detail?id=123&colspec=a b C')
mr.ComputeColSpec(config)
self.assertEqual('a b C', mr.col_spec)
config.default_col_spec = 'd e f'
# Default in the config, and nothing in URL
mr = testing_helpers.MakeMonorailRequest(
path='/p/proj/issues/detail?id=123')
mr.ComputeColSpec(config)
self.assertEqual('d e f', mr.col_spec)
# Default in the config, but overrided via URL
mr = testing_helpers.MakeMonorailRequest(
path='/p/proj/issues/detail?id=123&colspec=a b C')
mr.ComputeColSpec(config)
self.assertEqual('a b C', mr.col_spec)
# project colspec contains hotlist columns
mr = testing_helpers.MakeMonorailRequest(
path='p/proj/issues/detail?id=123&colspec=Rank Adder Adder Owner')
mr.ComputeColSpec(None)
self.assertEqual(tracker_constants.DEFAULT_COL_SPEC, mr.col_spec)
# hotlist columns are not deleted when page is a hotlist page
mr = testing_helpers.MakeMonorailRequest(
path='u/jrobbins@example.com/hotlists/TestHotlist?colspec=Rank Adder',
hotlist=self.hotlist)
mr.ComputeColSpec(None)
self.assertEqual('Rank Adder', mr.col_spec)
def testComputeColSpec_XSS(self):
config_1 = tracker_pb2.ProjectIssueConfig()
config_2 = tracker_pb2.ProjectIssueConfig()
config_2.default_col_spec = "id '+alert(1)+'"
mr_1 = testing_helpers.MakeMonorailRequest(
path='/p/proj/issues/detail?id=123')
mr_2 = testing_helpers.MakeMonorailRequest(
path="/p/proj/issues/detail?id=123&colspec=id '+alert(1)+'")
# Normal colspec in config but malicious request
self.assertRaises(
exceptions.InputException,
mr_2.ComputeColSpec, config_1)
# Malicious colspec in config but normal request
self.assertRaises(
exceptions.InputException,
mr_1.ComputeColSpec, config_2)
# Malicious colspec in config and malicious request
self.assertRaises(
exceptions.InputException,
mr_2.ComputeColSpec, config_2)
class CalcDefaultQueryTest(unittest.TestCase):
def setUp(self):
self.project = project_pb2.Project()
self.project.project_name = 'proj'
self.project.owner_ids = [111]
self.config = tracker_pb2.ProjectIssueConfig()
def testIssueListURL_NotDefaultCan(self):
mr = monorailrequest.MonorailRequest(None)
mr.query = None
mr.can = 1
self.assertEqual('', mr._CalcDefaultQuery())
def testIssueListURL_NoProject(self):
mr = monorailrequest.MonorailRequest(None)
mr.query = None
mr.can = 2
self.assertEqual('', mr._CalcDefaultQuery())
def testIssueListURL_NoConfig(self):
mr = monorailrequest.MonorailRequest(None)
mr.query = None
mr.can = 2
mr.project = self.project
self.assertEqual('', mr._CalcDefaultQuery())
def testIssueListURL_NotCustomized(self):
mr = monorailrequest.MonorailRequest(None)
mr.query = None
mr.can = 2
mr.project = self.project
mr.config = self.config
self.assertEqual('', mr._CalcDefaultQuery())
def testIssueListURL_Customized_Nonmember(self):
mr = monorailrequest.MonorailRequest(None)
mr.query = None
mr.can = 2
mr.project = self.project
mr.config = self.config
mr.config.member_default_query = 'owner:me'
self.assertEqual('', mr._CalcDefaultQuery())
mr.auth = testing_helpers.Blank(effective_ids=set())
self.assertEqual('', mr._CalcDefaultQuery())
mr.auth = testing_helpers.Blank(effective_ids={999})
self.assertEqual('', mr._CalcDefaultQuery())
def testIssueListURL_Customized_Member(self):
mr = monorailrequest.MonorailRequest(None)
mr.query = None
mr.can = 2
mr.project = self.project
mr.config = self.config
mr.config.member_default_query = 'owner:me'
mr.auth = testing_helpers.Blank(effective_ids={111})
self.assertEqual('owner:me', mr._CalcDefaultQuery())
class TestMonorailRequestFunctions(unittest.TestCase):
def testExtractPathIdentifiers_ProjectOnly(self):
(username, project_name, hotlist_id,
hotlist_name) = monorailrequest._ParsePathIdentifiers(
'/p/proj/issues/list?q=foo+OR+bar&ts=1234')
self.assertIsNone(username)
self.assertIsNone(hotlist_id)
self.assertIsNone(hotlist_name)
self.assertEqual('proj', project_name)
def testExtractPathIdentifiers_ViewedUserOnly(self):
(username, project_name, hotlist_id,
hotlist_name) = monorailrequest._ParsePathIdentifiers(
'/u/jrobbins@example.com/')
self.assertEqual('jrobbins@example.com', username)
self.assertIsNone(project_name)
self.assertIsNone(hotlist_id)
self.assertIsNone(hotlist_name)
def testExtractPathIdentifiers_ViewedUserURLSpace(self):
(username, project_name, hotlist_id,
hotlist_name) = monorailrequest._ParsePathIdentifiers(
'/u/jrobbins@example.com/updates')
self.assertEqual('jrobbins@example.com', username)
self.assertIsNone(project_name)
self.assertIsNone(hotlist_id)
self.assertIsNone(hotlist_name)
def testExtractPathIdentifiers_ViewedGroupURLSpace(self):
(username, project_name, hotlist_id,
hotlist_name) = monorailrequest._ParsePathIdentifiers(
'/g/user-group@example.com/updates')
self.assertEqual('user-group@example.com', username)
self.assertIsNone(project_name)
self.assertIsNone(hotlist_id)
self.assertIsNone(hotlist_name)
def testExtractPathIdentifiers_HotlistIssuesURLSpaceById(self):
(username, project_name, hotlist_id,
hotlist_name) = monorailrequest._ParsePathIdentifiers(
'/u/jrobbins@example.com/hotlists/13124?q=stuff&ts=more')
self.assertIsNone(hotlist_name)
self.assertIsNone(project_name)
self.assertEqual('jrobbins@example.com', username)
self.assertEqual(13124, hotlist_id)
def testExtractPathIdentifiers_HotlistIssuesURLSpaceByName(self):
(username, project_name, hotlist_id,
hotlist_name) = monorailrequest._ParsePathIdentifiers(
'/u/jrobbins@example.com/hotlists/testname?q=stuff&ts=more')
self.assertIsNone(project_name)
self.assertIsNone(hotlist_id)
self.assertEqual('jrobbins@example.com', username)
self.assertEqual('testname', hotlist_name)
def testParseColSpec(self):
parse = monorailrequest.ParseColSpec
self.assertEqual(['PageName', 'Summary', 'Changed', 'ChangedBy'],
parse(u'PageName Summary Changed ChangedBy'))
self.assertEqual(['Foo-Bar', 'Foo-Bar-Baz', 'Release-1.2', 'Hey', 'There'],
parse('Foo-Bar Foo-Bar-Baz Release-1.2 Hey!There'))
self.assertEqual(
['\xe7\xaa\xbf\xe8\x8b\xa5\xe7\xb9\xb9'.decode('utf-8'),
'\xe5\x9f\xba\xe5\x9c\xb0\xe3\x81\xaf'.decode('utf-8')],
parse('\xe7\xaa\xbf\xe8\x8b\xa5\xe7\xb9\xb9 '
'\xe5\x9f\xba\xe5\x9c\xb0\xe3\x81\xaf'.decode('utf-8')))
def testParseColSpec_Dedup(self):
"""An attacker cannot inflate response size by repeating a column."""
parse = monorailrequest.ParseColSpec
self.assertEqual([], parse(''))
self.assertEqual(
['Aa', 'b', 'c/d'],
parse(u'Aa Aa AA AA AA b Aa aa c/d d c aA b aa B C/D D/aa/c'))
self.assertEqual(
['A', 'b', 'c/d', 'e', 'f'],
parse(u'A b c/d e f g h i j a/k l m/c/a n/o'))
def testParseColSpec_Huge(self):
"""An attacker cannot inflate response size with a huge column name."""
parse = monorailrequest.ParseColSpec
self.assertEqual(
['Aa', 'b', 'c/d'],
parse(u'Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b c/d'))
def testParseColSpec_Ignore(self):
"""We ignore groupby and grid axes that would be useless."""
parse = monorailrequest.ParseColSpec
self.assertEqual(
['Aa', 'b', 'c/d'],
parse(u'Aa AllLabels alllabels Id b opened/summary c/d',
ignore=tracker_constants.NOT_USED_IN_GRID_AXES))
class TestPermissionLookup(unittest.TestCase):
OWNER_ID = 1
OTHER_USER_ID = 2
def setUp(self):
self.services = service_manager.Services(
project=fake.ProjectService(),
user=fake.UserService(),
usergroup=fake.UserGroupService())
self.services.user.TestAddUser('owner@gmail.com', self.OWNER_ID)
self.services.user.TestAddUser('user@gmail.com', self.OTHER_USER_ID)
self.live_project = self.services.project.TestAddProject(
'live', owner_ids=[self.OWNER_ID])
self.archived_project = self.services.project.TestAddProject(
'archived', owner_ids=[self.OWNER_ID],
state=project_pb2.ProjectState.ARCHIVED)
self.members_only_project = self.services.project.TestAddProject(
'members-only', owner_ids=[self.OWNER_ID],
access=project_pb2.ProjectAccess.MEMBERS_ONLY)
self.mox = mox.Mox()
def tearDown(self):
self.mox.UnsetStubs()
def CheckPermissions(self, perms, expect_view, expect_commit, expect_edit):
may_view = perms.HasPerm(permissions.VIEW, None, None)
self.assertEqual(expect_view, may_view)
may_commit = perms.HasPerm(permissions.COMMIT, None, None)
self.assertEqual(expect_commit, may_commit)
may_edit = perms.HasPerm(permissions.EDIT_PROJECT, None, None)
self.assertEqual(expect_edit, may_edit)
def MakeRequestAsUser(self, project_name, email):
self.mox.StubOutWithMock(users, 'get_current_user')
users.get_current_user().AndReturn(testing_helpers.Blank(
email=lambda: email))
self.mox.ReplayAll()
request = webapp2.Request.blank('/p/' + project_name)
mr = monorailrequest.MonorailRequest(self.services)
with mr.profiler.Phase('parse user info'):
mr.ParseRequest(request, self.services)
print('mr.auth is %r' % mr.auth)
return mr
def testOwnerPermissions_Live(self):
mr = self.MakeRequestAsUser('live', 'owner@gmail.com')
self.CheckPermissions(mr.perms, True, True, True)
def testOwnerPermissions_Archived(self):
mr = self.MakeRequestAsUser('archived', 'owner@gmail.com')
self.CheckPermissions(mr.perms, True, False, True)
def testOwnerPermissions_MembersOnly(self):
mr = self.MakeRequestAsUser('members-only', 'owner@gmail.com')
self.CheckPermissions(mr.perms, True, True, True)
def testExternalUserPermissions_Live(self):
mr = self.MakeRequestAsUser('live', 'user@gmail.com')
self.CheckPermissions(mr.perms, True, False, False)
def testExternalUserPermissions_Archived(self):
mr = self.MakeRequestAsUser('archived', 'user@gmail.com')
self.CheckPermissions(mr.perms, False, False, False)
def testExternalUserPermissions_MembersOnly(self):
mr = self.MakeRequestAsUser('members-only', 'user@gmail.com')
self.CheckPermissions(mr.perms, False, False, False)