blob: 7d628ba72c08c2cb3739fa6114ddb91fffc9195a [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2018 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Copybara854996b2021-09-07 19:36:02 +00004
5"""Tests for MonorailServicer."""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
10import time
11import unittest
12import mock
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020013try:
14 from mox3 import mox
15except ImportError:
16 import mox
Copybara854996b2021-09-07 19:36:02 +000017
Copybara854996b2021-09-07 19:36:02 +000018from components.prpc import codes
19from components.prpc import context
20from google.appengine.ext import testbed
21from google.protobuf import json_format
22
23import settings
24from api.v3 import monorail_servicer
25from framework import authdata
26from framework import exceptions
27from framework import framework_constants
28from framework import monorailcontext
29from framework import permissions
30from framework import ratelimiter
31from framework import xsrf
32from services import cachemanager_svc
33from services import config_svc
34from services import service_manager
35from services import features_svc
36from testing import fake
37from testing import testing_helpers
38
39
40class MonorailServicerFunctionsTest(unittest.TestCase):
41
42 def testConvertPRPCStatusToHTTPStatus(self):
43 """We can convert pRPC status codes to http codes for monitoring."""
44 prpc_context = context.ServicerContext()
45
46 prpc_context.set_code(codes.StatusCode.OK)
47 self.assertEqual(
48 200, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
49
50 prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
51 self.assertEqual(
52 400, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
53
54 prpc_context.set_code(codes.StatusCode.PERMISSION_DENIED)
55 self.assertEqual(
56 403, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
57
58 prpc_context.set_code(codes.StatusCode.NOT_FOUND)
59 self.assertEqual(
60 404, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
61
62 prpc_context.set_code(codes.StatusCode.INTERNAL)
63 self.assertEqual(
64 500, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
65
66
67class UpdateSomethingRequest(testing_helpers.Blank):
68 """A fake request that would do a write."""
69 pass
70
71
72class ListSomethingRequest(testing_helpers.Blank):
73 """A fake request that would do a read."""
74 pass
75
76
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010077class _TestableServicer(monorail_servicer.MonorailServicer):
Copybara854996b2021-09-07 19:36:02 +000078 """Fake servicer class."""
79
80 def __init__(self, services):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010081 super(_TestableServicer, self).__init__(services)
Copybara854996b2021-09-07 19:36:02 +000082 self.was_called = False
83 self.seen_mc = None
84 self.seen_request = None
85
86 @monorail_servicer.PRPCMethod
87 def CalcSomething(self, mc, request):
88 """Raise the test exception, or return what we got for verification."""
89 self.was_called = True
90 self.seen_mc = mc
91 self.seen_request = request
92 assert mc
93 assert request
94 if request.exc_class:
95 raise request.exc_class()
96 else:
97 return 'fake response proto'
98
99
100class MonorailServicerTest(unittest.TestCase):
101
102 def setUp(self):
103 self.mox = mox.Mox()
104 self.testbed = testbed.Testbed()
105 self.testbed.activate()
106 self.testbed.init_memcache_stub()
107 self.testbed.init_datastore_v3_stub()
108 self.testbed.init_user_stub()
109
110 self.cnxn = fake.MonorailConnection()
111 self.services = service_manager.Services(
112 user=fake.UserService(),
113 usergroup=fake.UserGroupService(),
114 project=fake.ProjectService(),
115 cache_manager=fake.CacheManager())
116 self.project = self.services.project.TestAddProject(
117 'proj', project_id=789, owner_ids=[111])
118 # Allowlisted in testing/api_clients.cfg
119 self.allowlisted_client_id = '98723764876'
120 self.non_member = self.services.user.TestAddUser(
121 'nonmember@example.com', 222)
122 self.test_user = self.services.user.TestAddUser('test@example.com', 420)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100123 self.svcr = _TestableServicer(self.services)
Copybara854996b2021-09-07 19:36:02 +0000124 self.nonmember_token = xsrf.GenerateToken(222, xsrf.XHR_SERVLET_PATH)
125 self.request = UpdateSomethingRequest(exc_class=None)
126 self.prpc_context = context.ServicerContext()
127 self.prpc_context.set_code(codes.StatusCode.OK)
128 self.prpc_context._invocation_metadata = [
129 (monorail_servicer.XSRF_TOKEN_HEADER, self.nonmember_token)]
130 # This string is returned by app_identity.get_application_id() when
131 # called in the test env.
132 self.app_id = 'testing-app'
133
134 def tearDown(self):
135 self.mox.UnsetStubs()
136 self.mox.ResetAll()
137 self.testbed.deactivate()
138
139 def SetUpRecordMonitoringStats(self):
140 self.mox.StubOutWithMock(json_format, 'MessageToJson')
141 json_format.MessageToJson(self.request).AndReturn('json of request')
142 json_format.MessageToJson('fake response proto').AndReturn(
143 'json of response')
144 self.mox.ReplayAll()
145
146 def testRun_SiteWide_Normal(self):
147 """Calling the handler through the decorator."""
148 self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
149 self.SetUpRecordMonitoringStats()
150 # pylint: disable=unexpected-keyword-arg
151 response = self.svcr.CalcSomething(
152 self.request, self.prpc_context, cnxn=self.cnxn)
153 self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
154 self.assertEqual(self.svcr.seen_mc.auth.email, self.non_member.email)
155 self.assertIn(permissions.CREATE_HOTLIST.lower(),
156 self.svcr.seen_mc.perms.perm_names)
157 self.assertNotIn(permissions.ADMINISTER_SITE.lower(),
158 self.svcr.seen_mc.perms.perm_names)
159 self.assertEqual(self.request, self.svcr.seen_request)
160 self.assertEqual('fake response proto', response)
161 self.assertEqual(codes.StatusCode.OK, self.prpc_context._code)
162
163 def testRun_RequesterBanned(self):
164 """If we reject the request, give PERMISSION_DENIED."""
165 self.non_member.banned = 'Spammer'
166 self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
167 self.SetUpRecordMonitoringStats()
168 # pylint: disable=unexpected-keyword-arg
169 self.svcr.CalcSomething(
170 self.request, self.prpc_context, cnxn=self.cnxn)
171 self.assertFalse(self.svcr.was_called)
172 self.assertEqual(
173 codes.StatusCode.PERMISSION_DENIED, self.prpc_context._code)
174
175 def testRun_AnonymousRequester(self):
176 """Test we properly process anonymous users with valid tokens."""
177 self.prpc_context._invocation_metadata = [
178 (monorail_servicer.XSRF_TOKEN_HEADER,
179 xsrf.GenerateToken(0, xsrf.XHR_SERVLET_PATH))]
180 self.SetUpRecordMonitoringStats()
181 # pylint: disable=unexpected-keyword-arg
182 response = self.svcr.CalcSomething(
183 self.request, self.prpc_context, cnxn=self.cnxn)
184 self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
185 self.assertIsNone(self.svcr.seen_mc.auth.email)
186 self.assertNotIn(permissions.CREATE_HOTLIST.lower(),
187 self.svcr.seen_mc.perms.perm_names)
188 self.assertNotIn(permissions.ADMINISTER_SITE.lower(),
189 self.svcr.seen_mc.perms.perm_names)
190 self.assertEqual(self.request, self.svcr.seen_request)
191 self.assertEqual('fake response proto', response)
192 self.assertEqual(codes.StatusCode.OK, self.prpc_context._code)
193
194 def testRun_DistributedInvalidation(self):
195 """The Run method must call DoDistributedInvalidation()."""
196 self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
197 self.SetUpRecordMonitoringStats()
198 # pylint: disable=unexpected-keyword-arg
199 self.svcr.CalcSomething(
200 self.request, self.prpc_context, cnxn=self.cnxn)
201 self.assertIsNotNone(self.services.cache_manager.last_call)
202
203 def testRun_HandlerErrorResponse(self):
204 """An expected exception in the method causes an error status."""
205 self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
206 self.SetUpRecordMonitoringStats()
207 # pylint: disable=attribute-defined-outside-init
208 self.request.exc_class = exceptions.NoSuchUserException
209 # pylint: disable=unexpected-keyword-arg
210 response = self.svcr.CalcSomething(
211 self.request, self.prpc_context, cnxn=self.cnxn)
212 self.assertTrue(self.svcr.was_called)
213 self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
214 self.assertEqual(self.svcr.seen_mc.auth.email, self.non_member.email)
215 self.assertEqual(self.request, self.svcr.seen_request)
216 self.assertIsNone(response)
217 self.assertEqual(codes.StatusCode.NOT_FOUND, self.prpc_context._code)
218
219 def testRun_HandlerProgrammingError(self):
220 """An unexception in the handler method is re-raised."""
221 self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
222 self.SetUpRecordMonitoringStats()
223 # pylint: disable=attribute-defined-outside-init
224 self.request.exc_class = NotImplementedError
225 self.assertRaises(
226 NotImplementedError,
227 self.svcr.CalcSomething,
228 self.request, self.prpc_context, cnxn=self.cnxn)
229 self.assertTrue(self.svcr.was_called)
230 self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
231
232 def testGetAndAssertRequesterAuth_Cookie_Anon(self):
233 """We get and allow requests from anon user using cookie auth."""
234 metadata = {
235 monorail_servicer.XSRF_TOKEN_HEADER: xsrf.GenerateToken(
236 0, xsrf.XHR_SERVLET_PATH)}
237 # Signed out.
238 client_id, user_auth = self.svcr.GetAndAssertRequesterAuth(
239 self.cnxn, metadata, self.services)
240 self.assertIsNone(user_auth.email)
241 self.assertEqual(client_id, 'https://%s.appspot.com' % self.app_id)
242
243 def testGetAndAssertRequesterAuth_Cookie_SignedIn(self):
244 """We get and allow requests from signed in users using cookie auth."""
245 metadata = dict(self.prpc_context.invocation_metadata())
246 # Signed in with cookie auth.
247 self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
248 client_id, user_auth = self.svcr.GetAndAssertRequesterAuth(
249 self.cnxn, metadata, self.services)
250 self.assertEqual(self.non_member.email, user_auth.email)
251 self.assertEqual(client_id, 'https://%s.appspot.com' % self.app_id)
252
253 def testGetAndAssertRequester_Anon_BadToken(self):
254 """We get the email address of the signed in user using oauth."""
255 metadata = {}
256 # Anonymous user has invalid token.
257 with self.assertRaises(permissions.PermissionException):
258 self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
259
260 @mock.patch('google.oauth2.id_token.verify_oauth2_token')
261 def testGetAndAssertRequesterAuth_IDToken_CaseInsensitiveBearer(
262 self, mock_verifier):
263 """We are case-insensitive when looking for the 'bearer' string."""
264 metadata = {'authorization': 'beaReR allowlisted-user-id-token'}
265 some_other_site_user = self.services.user.TestAddUser(
266 'some-human-user@human.test', 888)
267
268 # Signed in with oauth.
269 mock_verifier.return_value = {
270 'aud': self.allowlisted_client_id,
271 'email': some_other_site_user.email,
272 }
273
274 client_id, user_auth = self.svcr.GetAndAssertRequesterAuth(
275 self.cnxn, metadata, self.services)
276 self.assertEqual(client_id, self.allowlisted_client_id)
277 self.assertEqual(user_auth.email, some_other_site_user.email)
278 mock_verifier.assert_called_once_with('allowlisted-user-id-token', mock.ANY)
279
280 @mock.patch('google.oauth2.id_token.verify_oauth2_token')
281 def testGetAndAssertRequesterAuth_IDToken_AutoCreateUser(self, mock_verifier):
282 """We can auto-create Monorail users for the requester."""
283 metadata = {'authorization': 'beaReR allowlisted-user-id-token'}
284 # Signed in with oauth.
285 mock_verifier.return_value = {
286 'aud': self.allowlisted_client_id,
287 'email': 'new-user@email.com',
288 }
289
290 client_id, user_auth = self.svcr.GetAndAssertRequesterAuth(
291 self.cnxn, metadata, self.services)
292 self.assertEqual(client_id, self.allowlisted_client_id)
293 self.assertEqual(user_auth.email, 'new-user@email.com')
294 mock_verifier.assert_called_once_with('allowlisted-user-id-token', mock.ANY)
295
296 def testGetAndAssertRequesterAuth_IDToken_InvalidAuthToken(self):
297 """We raise an exception if 'bearer' is missing from headers."""
298 metadata = {'authorization': 'allowlisted-user-id-token'}
299
300 with self.assertRaises(permissions.PermissionException):
301 self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
302
303 @mock.patch('google.oauth2.id_token.verify_oauth2_token')
304 def testGetAndAssertRequesterAuth_IDToken_ServiceAccountAllowed(
305 self, mock_verifier):
306 """We allow requests from allowlisted service accounts with correct aud."""
307 metadata = {'authorization': 'Bearer allowlisted-user-id-token'}
308 # Allowlisted in testing/api_clients.cfg
309 allowlisted_service_account_email = self.services.user.TestAddUser(
310 '123456789@developer.gserviceaccount.com', 889)
311
312 aud = 'https://%s.appspot.com' % self.app_id
313 # Signed in with oauth.
314 mock_verifier.return_value = {
315 'aud': aud,
316 'email': allowlisted_service_account_email.email,
317 }
318
319 client_id, user_auth = self.svcr.GetAndAssertRequesterAuth(
320 self.cnxn, metadata, self.services)
321 self.assertEqual(client_id, aud)
322 self.assertEqual(user_auth.email, allowlisted_service_account_email.email)
323 mock_verifier.assert_called_once_with('allowlisted-user-id-token', mock.ANY)
324
325 @mock.patch('google.oauth2.id_token.verify_oauth2_token')
326 def testGetAndAssertRequesterAuth_IDToken_ServiceAccountNotAllowed(
327 self, mock_verifier):
328 """We raise an exception if the service account is not allowlisted"""
329 metadata = {'authorization': 'Bearer non-allowlisted-user-id-token'}
330
331 # Signed in with oauth.
332 mock_verifier.return_value = {
333 'aud': 'https://%s.appspot.com' % self.app_id,
334 # A random service account, not allow-listed.
335 'email': 'bigbadwolf@gserviceaccount.com',
336 }
337
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100338 with self.assertRaisesRegex(permissions.PermissionException,
339 r'Account .+ is not allowlisted'):
Copybara854996b2021-09-07 19:36:02 +0000340 self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
341
342 @mock.patch('google.oauth2.id_token.verify_oauth2_token')
343 def testGetAndAssertRequesterAuth_IDToken_ServiceAccountBadAud(
344 self, mock_verifier):
345 """We raise an exception when a service account token['aud'] is invalid."""
346 metadata = {'authorization': 'Bearer non-allowlisted-user-id-token'}
347 # Allowlisted in testing/api_clients.cfg
348 allowlisted_service_account_email = self.services.user.TestAddUser(
349 '123456789@developer.gserviceaccount.com', 889)
350
351 # Signed in with oauth.
352 mock_verifier.return_value = {
353 'aud': 'id-token-inteded-for-some-other-site',
354 'email': allowlisted_service_account_email.email,
355 }
356
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100357 with self.assertRaisesRegex(permissions.PermissionException,
358 r'Invalid token audience: .+'):
Copybara854996b2021-09-07 19:36:02 +0000359 self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
360
361 @mock.patch('google.oauth2.id_token.verify_oauth2_token')
362 def testGetAndAssertRequesterAuth_IDToken_ClientNotAllowed(
363 self, mock_verifier):
364 """We raise an exception if the client ID is not allowlisted."""
365 metadata = {'authorization': 'Bearer non-allowlisted-client-id-token'}
366
367 # Signed in with oauth.
368 mock_verifier.return_value = {
369 # A client ID not allow-listed.
370 'aud': 'some-other-site-client-id',
371 # Some human user that the client is impersonating for the request.
372 'email': 'some-other-site-user@test.com',
373 }
374
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100375 with self.assertRaisesRegex(permissions.PermissionException,
376 r'Client .+ is not allowlisted'):
Copybara854996b2021-09-07 19:36:02 +0000377 self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
378
379 # Assert some-other-site-user was not auto-created.
380 with self.assertRaises(exceptions.NoSuchUserException):
381 self.services.user.LookupUserID(
382 self.cnxn, 'some-other-site-user@test.com')
383
384 @mock.patch('google.oauth2.id_token.verify_oauth2_token')
385 def testGetAndAssertRequesterAuth_IDToken_NoEmail(self, mock_verifier):
386 """We raise an exception if ID token has no email information."""
387 metadata = {'authorization': 'Bearer allowlisted-user-id-token'}
388
389 # Signed in with oauth.
390 mock_verifier.return_value = {'aud': self.allowlisted_client_id}
391
392 with self.assertRaises(permissions.PermissionException):
393 self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
394
395 @mock.patch('google.oauth2.id_token.verify_oauth2_token')
396 def testGetAndAssertRequesterAuth_IDToken_InvalidIDToken(self, mock_verifier):
397 """We raise an exception if the ID token is invalid."""
398 metadata = {'authorization': 'Bearer bad-token'}
399
400 mock_verifier.side_effect = ValueError()
401
402 with self.assertRaises(permissions.PermissionException):
403 self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
404
405 def testGetAndAssertRequesterAuth_Banned(self):
406 self.non_member.banned = 'Spammer'
407 metadata = dict(self.prpc_context.invocation_metadata())
408 # Signed in with cookie auth.
409 self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
410 with self.assertRaises(permissions.BannedUserException):
411 self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
412
413 def testGetRequester_TestAccountOnAppspot(self):
414 """Specifying test_account is ignored on deployed server."""
415 # pylint: disable=attribute-defined-outside-init
416 metadata = {'x-test-account': 'test@example.com'}
417 with self.assertRaises(exceptions.InputException):
418 self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
419
420 def testGetRequester_TestAccountOnDev(self):
421 """For integration testing, we can set test_account on dev_server."""
422 try:
423 orig_local_mode = settings.local_mode
424 settings.local_mode = True
425
426 # pylint: disable=attribute-defined-outside-init
427 metadata = {'x-test-account': 'test@example.com'}
428 client_id, test_auth = self.svcr.GetAndAssertRequesterAuth(
429 self.cnxn, metadata, self.services)
430 self.assertEqual('test@example.com', test_auth.email)
431 self.assertEqual('https://%s.appspot.com' % self.app_id, client_id)
432
433 # pylint: disable=attribute-defined-outside-init
434 metadata = {'x-test-account': 'test@anythingelse.com'}
435 with self.assertRaises(exceptions.InputException):
436 self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
437 finally:
438 settings.local_mode = orig_local_mode
439
440 def testAssertBaseChecks_SiteIsReadOnly_Write(self):
441 """We reject writes and allow reads when site is read-only."""
442 orig_read_only = settings.read_only
443 try:
444 settings.read_only = True
445 metadata = {}
446 self.assertRaises(
447 permissions.PermissionException,
448 self.svcr.AssertBaseChecks, self.request, metadata)
449 finally:
450 settings.read_only = orig_read_only
451
452 def testAssertBaseChecks_SiteIsReadOnly_Read(self):
453 """We reject writes and allow reads when site is read-only."""
454 orig_read_only = settings.read_only
455 try:
456 settings.read_only = True
457 metadata = {monorail_servicer.XSRF_TOKEN_HEADER: self.nonmember_token}
458
459 # Our default request is an update.
460 with self.assertRaises(permissions.PermissionException):
461 self.svcr.AssertBaseChecks(self.request, metadata)
462
463 # A method name starting with "List" or "Get" will run OK.
464 self.request = ListSomethingRequest(exc_class=None)
465 self.svcr.AssertBaseChecks(self.request, metadata)
466 finally:
467 settings.read_only = orig_read_only
468
469 def CheckExceptionStatus(self, e, expected_code, details=None):
470 mc = monorailcontext.MonorailContext(self.services)
471 self.prpc_context.set_code(codes.StatusCode.OK)
472 processed = self.svcr.ProcessException(e, self.prpc_context, mc)
473 if expected_code:
474 self.assertTrue(processed)
475 self.assertEqual(expected_code, self.prpc_context._code)
476 else:
477 self.assertFalse(processed)
478 # Uncaught exceptions should indicate an error.
479 self.assertEqual(codes.StatusCode.INTERNAL, self.prpc_context._code)
480 if details is not None:
481 self.assertEqual(details, self.prpc_context._details)
482
483 def testProcessException(self):
484 """Expected exceptions are converted to pRPC codes, expected not."""
485 self.CheckExceptionStatus(
486 exceptions.NoSuchUserException(), codes.StatusCode.NOT_FOUND)
487 self.CheckExceptionStatus(
488 exceptions.NoSuchProjectException(), codes.StatusCode.NOT_FOUND)
489 self.CheckExceptionStatus(
490 exceptions.NoSuchIssueException(), codes.StatusCode.NOT_FOUND)
491 self.CheckExceptionStatus(
492 exceptions.NoSuchComponentException(), codes.StatusCode.NOT_FOUND)
493 self.CheckExceptionStatus(
494 permissions.BannedUserException(), codes.StatusCode.PERMISSION_DENIED)
495 self.CheckExceptionStatus(
496 permissions.PermissionException(), codes.StatusCode.PERMISSION_DENIED)
497 self.CheckExceptionStatus(
498 exceptions.GroupExistsException(), codes.StatusCode.ALREADY_EXISTS)
499 self.CheckExceptionStatus(
500 exceptions.InvalidComponentNameException(),
501 codes.StatusCode.INVALID_ARGUMENT)
502 self.CheckExceptionStatus(
503 exceptions.FilterRuleException(),
504 codes.StatusCode.INVALID_ARGUMENT,
505 details='Violates filter rule that should error.')
506 self.CheckExceptionStatus(
507 exceptions.InputException('echoed values'),
508 codes.StatusCode.INVALID_ARGUMENT,
509 details='Invalid arguments: echoed values')
510 self.CheckExceptionStatus(
511 exceptions.OverAttachmentQuota(), codes.StatusCode.RESOURCE_EXHAUSTED)
512 self.CheckExceptionStatus(
513 ratelimiter.ApiRateLimitExceeded('client_id', 'email'),
514 codes.StatusCode.PERMISSION_DENIED)
515 self.CheckExceptionStatus(
516 features_svc.HotlistAlreadyExists(), codes.StatusCode.ALREADY_EXISTS)
517 self.CheckExceptionStatus(NotImplementedError(), None)
518
519 def testProcessException_ErrorMessageEscaped(self):
520 """If we ever echo user input in error messages, it is escaped.."""
521 self.CheckExceptionStatus(
522 exceptions.InputException('echoed <script>"code"</script>'),
523 codes.StatusCode.INVALID_ARGUMENT,
524 details=('Invalid arguments: echoed '
525 '&lt;script&gt;&quot;code&quot;&lt;/script&gt;'))
526
527 def testRecordMonitoringStats_RequestClassDoesNotEndInRequest(self):
528 """We cope with request proto class names that do not end in 'Request'."""
529 self.request = 'this is a string'
530 self.SetUpRecordMonitoringStats()
531 start_time = 1522559788.939511
532 now = 1522569311.892738
533 self.svcr.RecordMonitoringStats(
534 start_time, self.request, 'fake response proto', self.prpc_context,
535 now=now)