1# Copyright 2016 Google LLC
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
15import unittest
16
17import mock
18
19
20def _make_credentials():
21    import google.auth.credentials
22
23    return mock.Mock(spec=google.auth.credentials.Credentials)
24
25
26class TestConnection(unittest.TestCase):
27
28    PROJECT = "project"
29    FILTER = "logName:syslog AND severity>=ERROR"
30
31    @staticmethod
32    def _get_default_timeout():
33        from google.cloud.logging_v2._http import _http
34
35        return _http._DEFAULT_TIMEOUT
36
37    @staticmethod
38    def _get_target_class():
39        from google.cloud.logging_v2._http import Connection
40
41        return Connection
42
43    def _make_one(self, *args, **kw):
44        return self._get_target_class()(*args, **kw)
45
46    def test_default_url(self):
47        client = object()
48        conn = self._make_one(client)
49        self.assertIs(conn._client, client)
50
51    def test_build_api_url_w_custom_endpoint(self):
52        from urllib.parse import parse_qsl
53        from urllib.parse import urlsplit
54
55        custom_endpoint = "https://foo-logging.googleapis.com"
56        conn = self._make_one(object(), api_endpoint=custom_endpoint)
57        uri = conn.build_api_url("/foo")
58        scheme, netloc, path, qs, _ = urlsplit(uri)
59        self.assertEqual("%s://%s" % (scheme, netloc), custom_endpoint)
60        self.assertEqual(path, "/".join(["", conn.API_VERSION, "foo"]))
61        parms = dict(parse_qsl(qs))
62        pretty_print = parms.pop("prettyPrint", "false")
63        self.assertEqual(pretty_print, "false")
64        self.assertEqual(parms, {})
65
66    def test_extra_headers(self):
67        import requests
68        from google.cloud import _http as base_http
69
70        http = mock.create_autospec(requests.Session, instance=True)
71        response = requests.Response()
72        response.status_code = 200
73        data = b"brent-spiner"
74        response._content = data
75        http.request.return_value = response
76        client = mock.Mock(_http=http, spec=["_http"])
77
78        conn = self._make_one(client)
79        req_data = "req-data-boring"
80        result = conn.api_request("GET", "/rainbow", data=req_data, expect_json=False)
81        self.assertEqual(result, data)
82
83        expected_headers = {
84            "Accept-Encoding": "gzip",
85            base_http.CLIENT_INFO_HEADER: conn.user_agent,
86            "User-Agent": conn.user_agent,
87        }
88        expected_uri = conn.build_api_url("/rainbow")
89        http.request.assert_called_once_with(
90            data=req_data,
91            headers=expected_headers,
92            method="GET",
93            url=expected_uri,
94            timeout=self._get_default_timeout(),
95        )
96
97
98class Test_LoggingAPI(unittest.TestCase):
99
100    PROJECT = "project"
101    PROJECT_PATH = "projects/project"
102    LIST_ENTRIES_PATH = "entries:list"
103    WRITE_ENTRIES_PATH = "entries:write"
104    LOGGER_NAME = "LOGGER_NAME"
105    LOGGER_PATH = "projects/project/logs/LOGGER_NAME"
106    FILTER = "logName:syslog AND severity>=ERROR"
107
108    @staticmethod
109    def _get_target_class():
110        from google.cloud.logging_v2._http import _LoggingAPI
111
112        return _LoggingAPI
113
114    def _make_one(self, *args, **kw):
115        return self._get_target_class()(*args, **kw)
116
117    def test_ctor(self):
118        connection = _Connection()
119        client = _Client(connection)
120        api = self._make_one(client)
121        self.assertIs(api._client, client)
122        self.assertEqual(api.api_request, connection.api_request)
123
124    @staticmethod
125    def _make_timestamp():
126        import datetime
127        from google.cloud._helpers import UTC
128
129        NOW = datetime.datetime.utcnow().replace(tzinfo=UTC)
130        return NOW, _datetime_to_rfc3339_w_nanos(NOW)
131
132    def test_list_entries_no_paging(self):
133        from google.cloud.logging import Client
134        from google.cloud.logging import TextEntry
135        from google.cloud.logging import Logger
136
137        NOW, TIMESTAMP = self._make_timestamp()
138        IID = "IID"
139        TEXT = "TEXT"
140        SENT = {"resourceNames": [self.PROJECT_PATH]}
141        TOKEN = "TOKEN"
142        RETURNED = {
143            "entries": [
144                {
145                    "textPayload": TEXT,
146                    "insertId": IID,
147                    "resource": {"type": "global"},
148                    "timestamp": TIMESTAMP,
149                    "logName": f"projects/{self.PROJECT}/logs/{self.LOGGER_NAME}",
150                }
151            ],
152            "nextPageToken": TOKEN,
153        }
154        client = Client(
155            project=self.PROJECT, credentials=_make_credentials(), _use_grpc=False
156        )
157        client._connection = _Connection(RETURNED)
158        api = self._make_one(client)
159
160        iterator = api.list_entries([self.PROJECT_PATH])
161        page = next(iterator.pages)
162        entries = list(page)
163        token = iterator.next_page_token
164
165        # First check the token.
166        self.assertEqual(token, TOKEN)
167        # Then check the entries returned.
168        self.assertEqual(len(entries), 1)
169        entry = entries[0]
170        self.assertIsInstance(entry, TextEntry)
171        self.assertEqual(entry.payload, TEXT)
172        self.assertIsInstance(entry.logger, Logger)
173        self.assertEqual(entry.logger.name, self.LOGGER_NAME)
174        self.assertEqual(entry.insert_id, IID)
175        self.assertEqual(entry.timestamp, NOW)
176        self.assertIsNone(entry.labels)
177        self.assertIsNone(entry.severity)
178        self.assertIsNone(entry.http_request)
179
180        called_with = client._connection._called_with
181        expected_path = "/%s" % (self.LIST_ENTRIES_PATH,)
182        self.assertEqual(
183            called_with, {"method": "POST", "path": expected_path, "data": SENT}
184        )
185
186    def test_list_entries_w_paging(self):
187        from google.cloud.logging import DESCENDING
188        from google.cloud.logging import Client
189        from google.cloud.logging import Logger
190        from google.cloud.logging import ProtobufEntry
191        from google.cloud.logging import StructEntry
192
193        PROJECT1 = "PROJECT1"
194        PROJECT1_PATH = f"projects/{PROJECT1}"
195        PROJECT2 = "PROJECT2"
196        PROJECT2_PATH = f"projects/{PROJECT2}"
197        NOW, TIMESTAMP = self._make_timestamp()
198        IID1 = "IID1"
199        IID2 = "IID2"
200        PAYLOAD = {"message": "MESSAGE", "weather": "partly cloudy"}
201        PROTO_PAYLOAD = PAYLOAD.copy()
202        PROTO_PAYLOAD["@type"] = "type.googleapis.com/testing.example"
203        TOKEN = "TOKEN"
204        PAGE_SIZE = 42
205        SENT = {
206            "resourceNames": [PROJECT1_PATH, PROJECT2_PATH],
207            "filter": self.FILTER,
208            "orderBy": DESCENDING,
209            "pageSize": PAGE_SIZE,
210            "pageToken": TOKEN,
211        }
212        RETURNED = {
213            "entries": [
214                {
215                    "jsonPayload": PAYLOAD,
216                    "insertId": IID1,
217                    "resource": {"type": "global"},
218                    "timestamp": TIMESTAMP,
219                    "logName": "projects/%s/logs/%s" % (self.PROJECT, self.LOGGER_NAME),
220                },
221                {
222                    "protoPayload": PROTO_PAYLOAD,
223                    "insertId": IID2,
224                    "resource": {"type": "global"},
225                    "timestamp": TIMESTAMP,
226                    "logName": "projects/%s/logs/%s" % (self.PROJECT, self.LOGGER_NAME),
227                },
228            ]
229        }
230        client = Client(
231            project=self.PROJECT, credentials=_make_credentials(), _use_grpc=False
232        )
233        client._connection = _Connection(RETURNED)
234        api = self._make_one(client)
235
236        iterator = api.list_entries(
237            resource_names=[PROJECT1_PATH, PROJECT2_PATH],
238            filter_=self.FILTER,
239            order_by=DESCENDING,
240            page_size=PAGE_SIZE,
241            page_token=TOKEN,
242        )
243        entries = list(iterator)
244        token = iterator.next_page_token
245
246        # First check the token.
247        self.assertIsNone(token)
248        # Then check the entries returned.
249        self.assertEqual(len(entries), 2)
250        entry1 = entries[0]
251        self.assertIsInstance(entry1, StructEntry)
252        self.assertEqual(entry1.payload, PAYLOAD)
253        self.assertIsInstance(entry1.logger, Logger)
254        self.assertEqual(entry1.logger.name, self.LOGGER_NAME)
255        self.assertEqual(entry1.insert_id, IID1)
256        self.assertEqual(entry1.timestamp, NOW)
257        self.assertIsNone(entry1.labels)
258        self.assertIsNone(entry1.severity)
259        self.assertIsNone(entry1.http_request)
260
261        entry2 = entries[1]
262        self.assertIsInstance(entry2, ProtobufEntry)
263        self.assertEqual(entry2.payload, PROTO_PAYLOAD)
264        self.assertIsInstance(entry2.logger, Logger)
265        self.assertEqual(entry2.logger.name, self.LOGGER_NAME)
266        self.assertEqual(entry2.insert_id, IID2)
267        self.assertEqual(entry2.timestamp, NOW)
268        self.assertIsNone(entry2.labels)
269        self.assertIsNone(entry2.severity)
270        self.assertIsNone(entry2.http_request)
271
272        called_with = client._connection._called_with
273        expected_path = "/%s" % (self.LIST_ENTRIES_PATH,)
274        self.assertEqual(
275            called_with, {"method": "POST", "path": expected_path, "data": SENT}
276        )
277
278    def test_write_entries_single(self):
279        TEXT = "TEXT"
280        ENTRY = {
281            "textPayload": TEXT,
282            "resource": {"type": "global"},
283            "logName": "projects/{self.PROJECT}/logs/{self.LOGGER_NAME}",
284        }
285        SENT = {"entries": [ENTRY], "partialSuccess": False, "dry_run": False}
286        conn = _Connection({})
287        client = _Client(conn)
288        api = self._make_one(client)
289
290        api.write_entries([ENTRY])
291
292        self.assertEqual(conn._called_with["method"], "POST")
293        path = f"/{self.WRITE_ENTRIES_PATH}"
294        self.assertEqual(conn._called_with["path"], path)
295        self.assertEqual(conn._called_with["data"], SENT)
296
297    def test_write_entries_multiple(self):
298        TEXT = "TEXT"
299        LOG_NAME = f"projects/{self.PROJECT}/logs/{self.LOGGER_NAME}"
300        RESOURCE = {"type": "global"}
301        LABELS = {"baz": "qux", "spam": "eggs"}
302        ENTRY1 = {"textPayload": TEXT}
303        ENTRY2 = {"jsonPayload": {"foo": "bar"}}
304        SENT = {
305            "logName": LOG_NAME,
306            "resource": RESOURCE,
307            "labels": LABELS,
308            "entries": [ENTRY1, ENTRY2],
309            "partialSuccess": False,
310            "dry_run": False,
311        }
312        conn = _Connection({})
313        client = _Client(conn)
314        api = self._make_one(client)
315
316        api.write_entries(
317            [ENTRY1, ENTRY2], logger_name=LOG_NAME, resource=RESOURCE, labels=LABELS
318        )
319
320        self.assertEqual(conn._called_with["method"], "POST")
321        path = f"/{self.WRITE_ENTRIES_PATH}"
322        self.assertEqual(conn._called_with["path"], path)
323        self.assertEqual(conn._called_with["data"], SENT)
324
325    def test_logger_delete(self):
326        path = f"/projects/{self.PROJECT}/logs/{self.LOGGER_NAME}"
327        conn = _Connection({})
328        client = _Client(conn)
329        api = self._make_one(client)
330
331        api.logger_delete(self.LOGGER_PATH)
332
333        self.assertEqual(conn._called_with["method"], "DELETE")
334        self.assertEqual(conn._called_with["path"], path)
335
336
337class Test_SinksAPI(unittest.TestCase):
338
339    PROJECT = "project"
340    PROJECT_PATH = "projects/project"
341    FILTER = "logName:syslog AND severity>=ERROR"
342    LIST_SINKS_PATH = f"projects/{PROJECT}/sinks"
343    SINK_NAME = "sink_name"
344    SINK_PATH = f"projects/{PROJECT}/sinks/{SINK_NAME}"
345    DESTINATION_URI = "faux.googleapis.com/destination"
346    WRITER_IDENTITY = "serviceAccount:project-123@example.com"
347
348    @staticmethod
349    def _get_target_class():
350        from google.cloud.logging_v2._http import _SinksAPI
351
352        return _SinksAPI
353
354    def _make_one(self, *args, **kw):
355        return self._get_target_class()(*args, **kw)
356
357    def test_ctor(self):
358        connection = _Connection()
359        client = _Client(connection)
360        api = self._make_one(client)
361        self.assertIs(api._client, client)
362        self.assertEqual(api.api_request, connection.api_request)
363
364    def test_list_sinks_no_paging(self):
365        from google.cloud.logging import Sink
366
367        TOKEN = "TOKEN"
368        RETURNED = {
369            "sinks": [
370                {
371                    "name": self.SINK_PATH,
372                    "filter": self.FILTER,
373                    "destination": self.DESTINATION_URI,
374                }
375            ],
376            "nextPageToken": TOKEN,
377        }
378        conn = _Connection(RETURNED)
379        client = _Client(conn)
380        api = self._make_one(client)
381
382        iterator = api.list_sinks(self.PROJECT_PATH)
383        page = next(iterator.pages)
384        sinks = list(page)
385        token = iterator.next_page_token
386
387        # First check the token.
388        self.assertEqual(token, TOKEN)
389        # Then check the sinks returned.
390        self.assertEqual(len(sinks), 1)
391        sink = sinks[0]
392        self.assertIsInstance(sink, Sink)
393        self.assertEqual(sink.name, self.SINK_PATH)
394        self.assertEqual(sink.filter_, self.FILTER)
395        self.assertEqual(sink.destination, self.DESTINATION_URI)
396        self.assertIs(sink.client, client)
397
398        called_with = conn._called_with
399        path = f"/{self.LIST_SINKS_PATH}"
400        self.assertEqual(
401            called_with, {"method": "GET", "path": path, "query_params": {}}
402        )
403
404    def test_list_sinks_w_paging(self):
405        from google.cloud.logging import Sink
406
407        TOKEN = "TOKEN"
408        PAGE_SIZE = 42
409        RETURNED = {
410            "sinks": [
411                {
412                    "name": self.SINK_PATH,
413                    "filter": self.FILTER,
414                    "destination": self.DESTINATION_URI,
415                }
416            ]
417        }
418        conn = _Connection(RETURNED)
419        client = _Client(conn)
420        api = self._make_one(client)
421
422        iterator = api.list_sinks(
423            self.PROJECT_PATH, page_size=PAGE_SIZE, page_token=TOKEN
424        )
425        sinks = list(iterator)
426        token = iterator.next_page_token
427
428        # First check the token.
429        self.assertIsNone(token)
430        # Then check the sinks returned.
431        self.assertEqual(len(sinks), 1)
432        sink = sinks[0]
433        self.assertIsInstance(sink, Sink)
434        self.assertEqual(sink.name, self.SINK_PATH)
435        self.assertEqual(sink.filter_, self.FILTER)
436        self.assertEqual(sink.destination, self.DESTINATION_URI)
437        self.assertIs(sink.client, client)
438
439        called_with = conn._called_with
440        path = f"/{self.LIST_SINKS_PATH}"
441        self.assertEqual(
442            called_with,
443            {
444                "method": "GET",
445                "path": path,
446                "query_params": {"pageSize": PAGE_SIZE, "pageToken": TOKEN},
447            },
448        )
449
450    def test_sink_create_conflict(self):
451        from google.cloud.exceptions import Conflict
452
453        sent = {
454            "name": self.SINK_NAME,
455            "filter": self.FILTER,
456            "destination": self.DESTINATION_URI,
457        }
458        conn = _Connection()
459        conn._raise_conflict = True
460        client = _Client(conn)
461        api = self._make_one(client)
462
463        with self.assertRaises(Conflict):
464            api.sink_create(
465                self.PROJECT_PATH, self.SINK_NAME, self.FILTER, self.DESTINATION_URI
466            )
467
468        path = f"/projects/{self.PROJECT}/sinks"
469        expected = {
470            "method": "POST",
471            "path": path,
472            "data": sent,
473            "query_params": {"uniqueWriterIdentity": False},
474        }
475        self.assertEqual(conn._called_with, expected)
476
477    def test_sink_create_ok(self):
478        sent = {
479            "name": self.SINK_NAME,
480            "filter": self.FILTER,
481            "destination": self.DESTINATION_URI,
482        }
483        after_create = sent.copy()
484        after_create["writerIdentity"] = self.WRITER_IDENTITY
485        conn = _Connection(after_create)
486        client = _Client(conn)
487        api = self._make_one(client)
488
489        returned = api.sink_create(
490            self.PROJECT_PATH,
491            self.SINK_NAME,
492            self.FILTER,
493            self.DESTINATION_URI,
494            unique_writer_identity=True,
495        )
496
497        self.assertEqual(returned, after_create)
498        path = f"/projects/{self.PROJECT}/sinks"
499        expected = {
500            "method": "POST",
501            "path": path,
502            "data": sent,
503            "query_params": {"uniqueWriterIdentity": True},
504        }
505        self.assertEqual(conn._called_with, expected)
506
507    def test_sink_get_miss(self):
508        from google.cloud.exceptions import NotFound
509
510        conn = _Connection()
511        client = _Client(conn)
512        api = self._make_one(client)
513
514        with self.assertRaises(NotFound):
515            api.sink_get(self.SINK_PATH)
516
517        self.assertEqual(conn._called_with["method"], "GET")
518        path = f"/projects/{self.PROJECT}/sinks/{self.SINK_NAME}"
519        self.assertEqual(conn._called_with["path"], path)
520
521    def test_sink_get_hit(self):
522        RESPONSE = {
523            "name": self.SINK_PATH,
524            "filter": self.FILTER,
525            "destination": self.DESTINATION_URI,
526        }
527        conn = _Connection(RESPONSE)
528        client = _Client(conn)
529        api = self._make_one(client)
530
531        response = api.sink_get(self.SINK_PATH)
532
533        self.assertEqual(response, RESPONSE)
534        self.assertEqual(conn._called_with["method"], "GET")
535        path = f"/projects/{self.PROJECT}/sinks/{self.SINK_NAME}"
536        self.assertEqual(conn._called_with["path"], path)
537
538    def test_sink_update_miss(self):
539        from google.cloud.exceptions import NotFound
540
541        sent = {
542            "name": self.SINK_NAME,
543            "filter": self.FILTER,
544            "destination": self.DESTINATION_URI,
545        }
546        conn = _Connection()
547        client = _Client(conn)
548        api = self._make_one(client)
549
550        with self.assertRaises(NotFound):
551            api.sink_update(self.SINK_PATH, self.FILTER, self.DESTINATION_URI)
552
553        path = f"/projects/{self.PROJECT}/sinks/{self.SINK_NAME}"
554        expected = {
555            "method": "PUT",
556            "path": path,
557            "data": sent,
558            "query_params": {"uniqueWriterIdentity": False},
559        }
560        self.assertEqual(conn._called_with, expected)
561
562    def test_sink_update_hit(self):
563        sent = {
564            "name": self.SINK_NAME,
565            "filter": self.FILTER,
566            "destination": self.DESTINATION_URI,
567        }
568        after_update = sent.copy()
569        after_update["writerIdentity"] = self.WRITER_IDENTITY
570        conn = _Connection(after_update)
571        client = _Client(conn)
572        api = self._make_one(client)
573
574        returned = api.sink_update(
575            self.SINK_PATH,
576            self.FILTER,
577            self.DESTINATION_URI,
578            unique_writer_identity=True,
579        )
580
581        self.assertEqual(returned, after_update)
582        path = f"/projects/{self.PROJECT}/sinks/{self.SINK_NAME}"
583        expected = {
584            "method": "PUT",
585            "path": path,
586            "data": sent,
587            "query_params": {"uniqueWriterIdentity": True},
588        }
589        self.assertEqual(conn._called_with, expected)
590
591    def test_sink_delete_miss(self):
592        from google.cloud.exceptions import NotFound
593
594        conn = _Connection()
595        client = _Client(conn)
596        api = self._make_one(client)
597
598        with self.assertRaises(NotFound):
599            api.sink_delete(self.SINK_PATH)
600
601        self.assertEqual(conn._called_with["method"], "DELETE")
602        path = f"/projects/{self.PROJECT}/sinks/{self.SINK_NAME}"
603        self.assertEqual(conn._called_with["path"], path)
604
605    def test_sink_delete_hit(self):
606        conn = _Connection({})
607        client = _Client(conn)
608        api = self._make_one(client)
609
610        api.sink_delete(self.SINK_PATH)
611
612        self.assertEqual(conn._called_with["method"], "DELETE")
613        path = f"/projects/{self.PROJECT}/sinks/{self.SINK_NAME}"
614        self.assertEqual(conn._called_with["path"], path)
615
616
617class Test_MetricsAPI(unittest.TestCase):
618
619    PROJECT = "project"
620    FILTER = "logName:syslog AND severity>=ERROR"
621    LIST_METRICS_PATH = "projects/%s/metrics" % (PROJECT,)
622    METRIC_NAME = "metric_name"
623    METRIC_PATH = "projects/%s/metrics/%s" % (PROJECT, METRIC_NAME)
624    DESCRIPTION = "DESCRIPTION"
625
626    @staticmethod
627    def _get_target_class():
628        from google.cloud.logging_v2._http import _MetricsAPI
629
630        return _MetricsAPI
631
632    def _make_one(self, *args, **kw):
633        return self._get_target_class()(*args, **kw)
634
635    def test_list_metrics_no_paging(self):
636        from google.cloud.logging import Metric
637
638        TOKEN = "TOKEN"
639        RETURNED = {
640            "metrics": [{"name": self.METRIC_PATH, "filter": self.FILTER}],
641            "nextPageToken": TOKEN,
642        }
643        conn = _Connection(RETURNED)
644        client = _Client(conn)
645        api = self._make_one(client)
646
647        iterator = api.list_metrics(self.PROJECT)
648        page = next(iterator.pages)
649        metrics = list(page)
650        token = iterator.next_page_token
651
652        # First check the token.
653        self.assertEqual(token, TOKEN)
654        # Then check the metrics returned.
655        self.assertEqual(len(metrics), 1)
656        metric = metrics[0]
657        self.assertIsInstance(metric, Metric)
658        self.assertEqual(metric.name, self.METRIC_PATH)
659        self.assertEqual(metric.filter_, self.FILTER)
660        self.assertEqual(metric.description, "")
661        self.assertIs(metric.client, client)
662
663        called_with = conn._called_with
664        path = "/%s" % (self.LIST_METRICS_PATH,)
665        self.assertEqual(
666            called_with, {"method": "GET", "path": path, "query_params": {}}
667        )
668
669    def test_list_metrics_w_paging(self):
670        from google.cloud.logging import Metric
671
672        TOKEN = "TOKEN"
673        PAGE_SIZE = 42
674        RETURNED = {"metrics": [{"name": self.METRIC_PATH, "filter": self.FILTER}]}
675        conn = _Connection(RETURNED)
676        client = _Client(conn)
677        api = self._make_one(client)
678
679        iterator = api.list_metrics(self.PROJECT, page_size=PAGE_SIZE, page_token=TOKEN)
680        metrics = list(iterator)
681        token = iterator.next_page_token
682
683        # First check the token.
684        self.assertIsNone(token)
685        # Then check the metrics returned.
686        self.assertEqual(len(metrics), 1)
687        metric = metrics[0]
688        self.assertIsInstance(metric, Metric)
689        self.assertEqual(metric.name, self.METRIC_PATH)
690        self.assertEqual(metric.filter_, self.FILTER)
691        self.assertEqual(metric.description, "")
692        self.assertIs(metric.client, client)
693
694        called_with = conn._called_with
695        path = "/%s" % (self.LIST_METRICS_PATH,)
696        self.assertEqual(
697            called_with,
698            {
699                "method": "GET",
700                "path": path,
701                "query_params": {"pageSize": PAGE_SIZE, "pageToken": TOKEN},
702            },
703        )
704
705    def test_metric_create_conflict(self):
706        from google.cloud.exceptions import Conflict
707
708        SENT = {
709            "name": self.METRIC_NAME,
710            "filter": self.FILTER,
711            "description": self.DESCRIPTION,
712        }
713        conn = _Connection()
714        conn._raise_conflict = True
715        client = _Client(conn)
716        api = self._make_one(client)
717
718        with self.assertRaises(Conflict):
719            api.metric_create(
720                self.PROJECT, self.METRIC_NAME, self.FILTER, self.DESCRIPTION
721            )
722
723        self.assertEqual(conn._called_with["method"], "POST")
724        path = "/projects/%s/metrics" % (self.PROJECT,)
725        self.assertEqual(conn._called_with["path"], path)
726        self.assertEqual(conn._called_with["data"], SENT)
727
728    def test_metric_create_ok(self):
729        SENT = {
730            "name": self.METRIC_NAME,
731            "filter": self.FILTER,
732            "description": self.DESCRIPTION,
733        }
734        conn = _Connection({})
735        client = _Client(conn)
736        api = self._make_one(client)
737
738        api.metric_create(self.PROJECT, self.METRIC_NAME, self.FILTER, self.DESCRIPTION)
739
740        self.assertEqual(conn._called_with["method"], "POST")
741        path = "/projects/%s/metrics" % (self.PROJECT,)
742        self.assertEqual(conn._called_with["path"], path)
743        self.assertEqual(conn._called_with["data"], SENT)
744
745    def test_metric_get_miss(self):
746        from google.cloud.exceptions import NotFound
747
748        conn = _Connection()
749        client = _Client(conn)
750        api = self._make_one(client)
751
752        with self.assertRaises(NotFound):
753            api.metric_get(self.PROJECT, self.METRIC_NAME)
754
755        self.assertEqual(conn._called_with["method"], "GET")
756        path = "/projects/%s/metrics/%s" % (self.PROJECT, self.METRIC_NAME)
757        self.assertEqual(conn._called_with["path"], path)
758
759    def test_metric_get_hit(self):
760        RESPONSE = {
761            "name": self.METRIC_NAME,
762            "filter": self.FILTER,
763            "description": self.DESCRIPTION,
764        }
765        conn = _Connection(RESPONSE)
766        client = _Client(conn)
767        api = self._make_one(client)
768
769        response = api.metric_get(self.PROJECT, self.METRIC_NAME)
770
771        self.assertEqual(response, RESPONSE)
772        self.assertEqual(conn._called_with["method"], "GET")
773        path = "/projects/%s/metrics/%s" % (self.PROJECT, self.METRIC_NAME)
774        self.assertEqual(conn._called_with["path"], path)
775
776    def test_metric_update_miss(self):
777        from google.cloud.exceptions import NotFound
778
779        SENT = {
780            "name": self.METRIC_NAME,
781            "filter": self.FILTER,
782            "description": self.DESCRIPTION,
783        }
784        conn = _Connection()
785        client = _Client(conn)
786        api = self._make_one(client)
787
788        with self.assertRaises(NotFound):
789            api.metric_update(
790                self.PROJECT, self.METRIC_NAME, self.FILTER, self.DESCRIPTION
791            )
792
793        self.assertEqual(conn._called_with["method"], "PUT")
794        path = "/projects/%s/metrics/%s" % (self.PROJECT, self.METRIC_NAME)
795        self.assertEqual(conn._called_with["path"], path)
796        self.assertEqual(conn._called_with["data"], SENT)
797
798    def test_metric_update_hit(self):
799        SENT = {
800            "name": self.METRIC_NAME,
801            "filter": self.FILTER,
802            "description": self.DESCRIPTION,
803        }
804        conn = _Connection({})
805        client = _Client(conn)
806        api = self._make_one(client)
807
808        api.metric_update(self.PROJECT, self.METRIC_NAME, self.FILTER, self.DESCRIPTION)
809
810        self.assertEqual(conn._called_with["method"], "PUT")
811        path = "/projects/%s/metrics/%s" % (self.PROJECT, self.METRIC_NAME)
812        self.assertEqual(conn._called_with["path"], path)
813        self.assertEqual(conn._called_with["data"], SENT)
814
815    def test_metric_delete_miss(self):
816        from google.cloud.exceptions import NotFound
817
818        conn = _Connection()
819        client = _Client(conn)
820        api = self._make_one(client)
821
822        with self.assertRaises(NotFound):
823            api.metric_delete(self.PROJECT, self.METRIC_NAME)
824
825        self.assertEqual(conn._called_with["method"], "DELETE")
826        path = "/projects/%s/metrics/%s" % (self.PROJECT, self.METRIC_NAME)
827        self.assertEqual(conn._called_with["path"], path)
828
829    def test_metric_delete_hit(self):
830        conn = _Connection({})
831        client = _Client(conn)
832        api = self._make_one(client)
833
834        api.metric_delete(self.PROJECT, self.METRIC_NAME)
835
836        self.assertEqual(conn._called_with["method"], "DELETE")
837        path = "/projects/%s/metrics/%s" % (self.PROJECT, self.METRIC_NAME)
838        self.assertEqual(conn._called_with["path"], path)
839
840
841class _Connection(object):
842
843    _called_with = None
844    _raise_conflict = False
845
846    def __init__(self, *responses):
847        self._responses = responses
848
849    def api_request(self, **kw):
850        from google.cloud.exceptions import Conflict
851        from google.cloud.exceptions import NotFound
852
853        self._called_with = kw
854        if self._raise_conflict:
855            raise Conflict("oops")
856        try:
857            response, self._responses = self._responses[0], self._responses[1:]
858        except IndexError:
859            raise NotFound("miss")
860        return response
861
862
863def _datetime_to_rfc3339_w_nanos(value):
864    from google.cloud._helpers import _RFC3339_NO_FRACTION
865
866    no_fraction = value.strftime(_RFC3339_NO_FRACTION)
867    return "%s.%09dZ" % (no_fraction, value.microsecond * 1000)
868
869
870class _Client(object):
871    def __init__(self, connection):
872        self._connection = connection
873