1# Copyright 2016 Google Inc. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15from __future__ import absolute_import 16 17import datetime 18import mock 19import os 20import tempfile 21import unittest2 22from expects import be_false, be_none, be_true, expect, equal, raise_error 23 24from google.api.control import caches, check_request, client, messages, report_request 25 26 27class TestSimpleLoader(unittest2.TestCase): 28 SERVICE_NAME = 'simpler-loader' 29 30 @mock.patch("google.api.control.client.ReportOptions", autospec=True) 31 @mock.patch("google.api.control.client.CheckOptions", autospec=True) 32 def test_should_create_client_ok(self, check_opts, report_opts): 33 # the mocks return fake instances else code using them fails 34 check_opts.return_value = caches.CheckOptions() 35 report_opts.return_value = caches.ReportOptions() 36 37 # ensure the client is constructed using no args instances of the opts 38 expect(client.Loaders.DEFAULT.load(self.SERVICE_NAME)).not_to(be_none) 39 check_opts.assert_called_once_with() 40 report_opts.assert_called_once_with() 41 42_TEST_CONFIG = """{ 43 "checkAggregatorConfig": { 44 "cacheEntries": 10, 45 "responseExpirationMs": 1000, 46 "flushIntervalMs": 2000 47 }, 48 "reportAggregatorConfig": { 49 "cacheEntries": 10, 50 "flushIntervalMs": 1000 51 } 52} 53""" 54 55 56class TestEnvironmentLoader(unittest2.TestCase): 57 SERVICE_NAME = 'environment-loader' 58 59 def setUp(self): 60 json_fd = tempfile.NamedTemporaryFile(delete=False) 61 with json_fd as f: 62 f.write(_TEST_CONFIG) 63 self._config_file = json_fd.name 64 os.environ[client.CONFIG_VAR] = self._config_file 65 66 def tearDown(self): 67 if os.path.exists(self._config_file): 68 os.remove(self._config_file) 69 70 @mock.patch("google.api.control.client.ReportOptions", autospec=True) 71 @mock.patch("google.api.control.client.CheckOptions", autospec=True) 72 def test_should_create_client_from_environment_ok(self, check_opts, report_opts): 73 check_opts.return_value = caches.CheckOptions() 74 report_opts.return_value = caches.ReportOptions() 75 76 # ensure the client is constructed using options values from the test JSON 77 expect(client.Loaders.ENVIRONMENT.load(self.SERVICE_NAME)).not_to(be_none) 78 check_opts.assert_called_once_with(expiration=datetime.timedelta(0, 1), 79 flush_interval=datetime.timedelta(0, 2), 80 num_entries=10) 81 report_opts.assert_called_once_with(flush_interval=datetime.timedelta(0, 1), 82 num_entries=10) 83 84 @mock.patch("google.api.control.client.ReportOptions", autospec=True) 85 @mock.patch("google.api.control.client.CheckOptions", autospec=True) 86 def test_should_use_defaults_if_file_is_missing(self, check_opts, report_opts): 87 os.remove(self._config_file) 88 self._assert_called_with_no_args_options(check_opts, report_opts) 89 90 @mock.patch("google.api.control.client.ReportOptions", autospec=True) 91 @mock.patch("google.api.control.client.CheckOptions", autospec=True) 92 def test_should_use_defaults_if_file_is_missing(self, check_opts, report_opts): 93 del os.environ[client.CONFIG_VAR] 94 self._assert_called_with_no_args_options(check_opts, report_opts) 95 96 @mock.patch("google.api.control.client.ReportOptions") 97 @mock.patch("google.api.control.client.CheckOptions") 98 def test_should_use_defaults_if_json_is_bad(self, check_opts, report_opts): 99 with open(self._config_file, 'w') as f: 100 f.write(_TEST_CONFIG + '\n{ this will not parse as json}') 101 self._assert_called_with_no_args_options(check_opts, report_opts) 102 103 def _assert_called_with_no_args_options(self, check_opts, report_opts): 104 # the mocks return fake instances else code using them fails 105 check_opts.return_value = caches.CheckOptions() 106 report_opts.return_value = caches.ReportOptions() 107 108 # ensure the client is constructed using no args instances of the opts 109 expect(client.Loaders.ENVIRONMENT.load(self.SERVICE_NAME)).not_to(be_none) 110 check_opts.assert_called_once_with() 111 report_opts.assert_called_once_with() 112 113 114def _make_dummy_report_request(project_id, service_name): 115 rules = report_request.ReportingRules() 116 info = report_request.Info( 117 consumer_project_id=project_id, 118 operation_id='an_op_id', 119 operation_name='an_op_name', 120 method='GET', 121 referer='a_referer', 122 service_name=service_name) 123 return info.as_report_request(rules) 124 125 126def _make_dummy_check_request(project_id, service_name): 127 info = check_request.Info( 128 consumer_project_id=project_id, 129 operation_id='an_op_id', 130 operation_name='an_op_name', 131 referer='a_referer', 132 service_name=service_name) 133 return info.as_check_request() 134 135 136class TestClientStartAndStop(unittest2.TestCase): 137 SERVICE_NAME = 'start-and-stop' 138 PROJECT_ID = SERVICE_NAME + '.project' 139 140 def setUp(self): 141 self._mock_transport = mock.MagicMock() 142 self._subject = client.Loaders.DEFAULT.load( 143 self.SERVICE_NAME, 144 create_transport=lambda: self._mock_transport) 145 146 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 147 def test_should_create_a_thread_when_started(self, thread_class): 148 self._subject.start() 149 expect(thread_class.called).to(be_true) 150 expect(len(thread_class.call_args_list)).to(equal(1)) 151 152 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 153 def test_should_only_create_thread_on_first_start(self, thread_class): 154 self._subject.start() 155 self._subject.start() 156 expect(len(thread_class.call_args_list)).to(equal(1)) 157 158 def test_should_noop_stop_if_not_started(self): 159 # stop the subject, the transport should not see a request 160 self._subject.stop() 161 expect(self._mock_transport.called).to(be_false) 162 163 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 164 def test_should_clear_requests_on_stop(self, dummy_thread_class): 165 # stop the subject, the transport did not see a request 166 self._subject.start() 167 self._subject.report( 168 _make_dummy_report_request(self.PROJECT_ID, self.SERVICE_NAME)) 169 self._subject.stop() 170 expect(self._mock_transport.services.report.called).to(be_true) 171 172 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 173 def test_should_ignore_stop_if_already_stopped(self, dummy_thread_class): 174 # stop the subject, the transport did not see a request 175 self._subject.start() 176 self._subject.report( 177 _make_dummy_report_request(self.PROJECT_ID, self.SERVICE_NAME)) 178 self._subject.stop() 179 self._mock_transport.reset_mock() 180 self._subject.stop() 181 expect(self._mock_transport.services.report.called).to(be_false) 182 183 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 184 def test_should_ignore_bad_transport_when_not_cached(self, dummy_thread_class): 185 self._subject.start() 186 self._subject.report( 187 _make_dummy_report_request(self.PROJECT_ID, self.SERVICE_NAME)) 188 self._mock_transport.services.report.side_effect = lambda: 1/0 189 self._subject.stop() 190 expect(self._mock_transport.services.report.called).to(be_true) 191 192 193class TestClientCheck(unittest2.TestCase): 194 SERVICE_NAME = 'check' 195 PROJECT_ID = SERVICE_NAME + '.project' 196 197 def setUp(self): 198 self._mock_transport = mock.MagicMock() 199 self._subject = client.Loaders.DEFAULT.load( 200 self.SERVICE_NAME, 201 create_transport=lambda: self._mock_transport) 202 203 def test_should_raise_on_check_without_start(self): 204 dummy_request = _make_dummy_check_request(self.PROJECT_ID, 205 self.SERVICE_NAME) 206 expect(lambda: self._subject.check(dummy_request)).to( 207 raise_error(AssertionError)) 208 209 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 210 def test_should_send_the_request_if_not_cached(self, dummy_thread_class): 211 self._subject.start() 212 dummy_request = _make_dummy_check_request(self.PROJECT_ID, 213 self.SERVICE_NAME) 214 self._subject.check(dummy_request) 215 expect(self._mock_transport.services.check.called).to(be_true) 216 217 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 218 def test_should_not_send_the_request_if_cached(self, dummy_thread_class): 219 t = self._mock_transport 220 self._subject.start() 221 dummy_request = _make_dummy_check_request(self.PROJECT_ID, 222 self.SERVICE_NAME) 223 dummy_response = messages.CheckResponse( 224 operationId=dummy_request.checkRequest.operation.operationId) 225 t.services.check.return_value = dummy_response 226 expect(self._subject.check(dummy_request)).to(equal(dummy_response)) 227 t.reset_mock() 228 expect(self._subject.check(dummy_request)).to(equal(dummy_response)) 229 expect(t.services.check.called).to(be_false) 230 231 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 232 def test_should_return_null_if_transport_fails(self, dummy_thread_class): 233 self._subject.start() 234 dummy_request = _make_dummy_check_request(self.PROJECT_ID, 235 self.SERVICE_NAME) 236 self._mock_transport.services.check.side_effect = lambda: 1/0 237 expect(self._subject.check(dummy_request)).to(be_none) 238 239 240class TestClientReport(unittest2.TestCase): 241 SERVICE_NAME = 'report' 242 PROJECT_ID = SERVICE_NAME + '.project' 243 244 def setUp(self): 245 self._mock_transport = mock.MagicMock() 246 self._subject = client.Loaders.DEFAULT.load( 247 self.SERVICE_NAME, 248 create_transport=lambda: self._mock_transport) 249 250 def test_should_raise_on_report_without_start(self): 251 dummy_request = _make_dummy_report_request(self.PROJECT_ID, 252 self.SERVICE_NAME) 253 expect(lambda: self._subject.report(dummy_request)).to( 254 raise_error(AssertionError)) 255 256 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 257 def test_should_not_send_the_request_if_cached(self, dummy_thread_class): 258 t = self._mock_transport 259 self._subject.start() 260 dummy_request = _make_dummy_report_request(self.PROJECT_ID, 261 self.SERVICE_NAME) 262 self._subject.report(dummy_request) 263 expect(t.services.report.called).to(be_false) 264 265 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 266 def test_should_send_a_request_if_not_cached(self, dummy_thread_class): 267 self._subject = client.Loaders.NO_CACHE.load( 268 self.SERVICE_NAME, 269 create_transport=lambda: self._mock_transport) 270 271 t = self._mock_transport 272 self._subject.start() 273 dummy_request = _make_dummy_report_request(self.PROJECT_ID, 274 self.SERVICE_NAME) 275 self._subject.report(dummy_request) 276 expect(t.services.report.called).to(be_true) 277 278 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 279 def test_should_ignore_bad_transport_when_not_cached(self, dummy_thread_class): 280 self._subject = client.Loaders.NO_CACHE.load( 281 self.SERVICE_NAME, 282 create_transport=lambda: self._mock_transport) 283 284 self._mock_transport.services.report.side_effect = lambda: 1/0 285 self._subject.start() 286 dummy_request = _make_dummy_report_request(self.PROJECT_ID, 287 self.SERVICE_NAME) 288 self._subject.report(dummy_request) 289 expect(self._mock_transport.services.report.called).to(be_true) 290 291 292class TestNoSchedulerThread(unittest2.TestCase): 293 SERVICE_NAME = 'no-scheduler-thread' 294 PROJECT_ID = SERVICE_NAME + '.project' 295 296 def setUp(self): 297 self._timer = _DateTimeTimer() 298 self._mock_transport = mock.MagicMock() 299 self._subject = client.Loaders.DEFAULT.load( 300 self.SERVICE_NAME, 301 create_transport=lambda: self._mock_transport, 302 timer=self._timer) 303 self._no_cache_subject = client.Loaders.NO_CACHE.load( 304 self.SERVICE_NAME, 305 create_transport=lambda: self._mock_transport, 306 timer=self._timer) 307 308 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 309 @mock.patch("google.api.control.client.sched", spec=True) 310 def test_should_initialize_scheduler(self, sched, thread_class): 311 thread_class.return_value.start.side_effect = lambda: 1/0 312 for s in (self._subject, self._no_cache_subject): 313 s.start() 314 expect(sched.scheduler.called).to(be_true) 315 sched.reset_mock() 316 317 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 318 @mock.patch("google.api.control.client.sched", spec=True) 319 def test_should_not_enter_scheduler_when_there_is_no_cache(self, sched, thread_class): 320 thread_class.return_value.start.side_effect = lambda: 1/0 321 self._no_cache_subject.start() 322 expect(sched.scheduler.called).to(be_true) 323 scheduler = sched.scheduler.return_value 324 expect(scheduler.enter.called).to(be_false) 325 326 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 327 @mock.patch("google.api.control.client.sched", spec=True) 328 def test_should_enter_scheduler_when_there_is_a_cache(self, sched, thread_class): 329 thread_class.return_value.start.side_effect = lambda: 1/0 330 self._subject.start() 331 expect(sched.scheduler.called).to(be_true) 332 scheduler = sched.scheduler.return_value 333 expect(scheduler.enter.called).to(be_true) 334 335 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 336 @mock.patch("google.api.control.client.sched", spec=True) 337 def test_should_not_enter_scheduler_for_cached_checks(self, sched, thread_class): 338 thread_class.return_value.start.side_effect = lambda: 1/0 339 self._subject.start() 340 341 # confirm scheduler is created and initialized 342 expect(sched.scheduler.called).to(be_true) 343 scheduler = sched.scheduler.return_value 344 expect(scheduler.enter.called).to(be_true) 345 scheduler.reset_mock() 346 347 # call check once, to a cache response 348 dummy_request = _make_dummy_check_request(self.PROJECT_ID, 349 self.SERVICE_NAME) 350 dummy_response = messages.CheckResponse( 351 operationId=dummy_request.checkRequest.operation.operationId) 352 t = self._mock_transport 353 t.services.check.return_value = dummy_response 354 expect(self._subject.check(dummy_request)).to(equal(dummy_response)) 355 t.reset_mock() 356 357 # call check again - response is cached... 358 expect(self._subject.check(dummy_request)).to(equal(dummy_response)) 359 expect(self._mock_transport.services.check.called).to(be_false) 360 361 # ... the scheduler is not run 362 expect(scheduler.run.called).to(be_false) 363 364 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 365 @mock.patch("google.api.control.client.sched", spec=True) 366 def test_should_enter_scheduler_for_aggregated_reports(self, sched, thread_class): 367 thread_class.return_value.start.side_effect = lambda: 1/0 368 self._subject.start() 369 370 # confirm scheduler is created and initialized 371 expect(sched.scheduler.called).to(be_true) 372 scheduler = sched.scheduler.return_value 373 expect(scheduler.enter.called).to(be_true) 374 scheduler.reset_mock() 375 376 # call report once; transport is not called, but the scheduler is run 377 dummy_request = _make_dummy_report_request(self.PROJECT_ID, 378 self.SERVICE_NAME) 379 self._subject.report(dummy_request) 380 expect(self._mock_transport.services.report.called).to(be_false) 381 expect(scheduler.run.called).to(be_true) 382 383 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 384 def test_should_flush_report_cache_in_scheduler(self, thread_class): 385 thread_class.return_value.start.side_effect = lambda: 1/0 386 self._subject.start() 387 388 # call report once; transport is not called 389 dummy_request = _make_dummy_report_request(self.PROJECT_ID, 390 self.SERVICE_NAME) 391 self._subject.report(dummy_request) # cached a report 392 expect(self._mock_transport.services.report.called).to(be_false) 393 # pass time, at least the flush interval, after which the report 394 # cache to flush 395 self._timer.tick() 396 self._timer.tick() 397 self._subject.report(dummy_request) 398 expect(self._mock_transport.services.report.called).to(be_true) 399 400 @mock.patch("google.api.control.client._THREAD_CLASS", spec=True) 401 @mock.patch("google.api.control.client.sched", spec=True) 402 def test_should_not_run_scheduler_when_stopping(self, sched, thread_class): 403 thread_class.return_value.start.side_effect = lambda: 1/0 404 self._subject.start() 405 406 # confirm scheduler is created and initialized 407 expect(sched.scheduler.called).to(be_true) 408 scheduler = sched.scheduler.return_value 409 expect(scheduler.enter.called).to(be_true) 410 411 # stop the subject. transport is called, but the scheduler is not run 412 self._subject.report( 413 _make_dummy_report_request(self.PROJECT_ID, self.SERVICE_NAME)) 414 scheduler.reset_mock() 415 self._subject.stop() 416 expect(self._mock_transport.services.report.called).to(be_true) 417 expect(scheduler.run.called).to(be_false) 418 419 420class _DateTimeTimer(object): 421 def __init__(self, auto=False): 422 self.auto = auto 423 self.time = datetime.datetime.utcfromtimestamp(0) 424 425 def __call__(self): 426 if self.auto: 427 self.tick() 428 return self.time 429 430 def tick(self): 431 self.time += datetime.timedelta(seconds=1) 432