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