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