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