|
32 | 32 | import json |
33 | 33 | import os |
34 | 34 | import pickle |
| 35 | +import pytest |
35 | 36 | import re |
36 | 37 | import sys |
37 | 38 | import unittest |
|
40 | 41 |
|
41 | 42 | import google.api_core.exceptions |
42 | 43 | import google.auth.credentials |
| 44 | +from google.auth import __version__ as auth_version |
43 | 45 | from google.auth.exceptions import MutualTLSChannelError |
44 | 46 | import google_auth_httplib2 |
45 | 47 | import httplib2 |
|
62 | 64 | HAS_UNIVERSE = False |
63 | 65 |
|
64 | 66 | from googleapiclient import _helpers as util |
65 | | -from googleapiclient.discovery import ( |
66 | | - DISCOVERY_URI, |
67 | | - MEDIA_BODY_PARAMETER_DEFAULT_VALUE, |
68 | | - MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE, |
69 | | - STACK_QUERY_PARAMETER_DEFAULT_VALUE, |
70 | | - STACK_QUERY_PARAMETERS, |
71 | | - V1_DISCOVERY_URI, |
72 | | - V2_DISCOVERY_URI, |
73 | | - APICoreVersionError, |
74 | | - ResourceMethodParameters, |
75 | | - _fix_up_media_path_base_url, |
76 | | - _fix_up_media_upload, |
77 | | - _fix_up_method_description, |
78 | | - _fix_up_parameters, |
79 | | - _urljoin, |
80 | | - build, |
81 | | - build_from_document, |
82 | | - key2param, |
83 | | -) |
| 67 | +from googleapiclient.discovery import (DISCOVERY_URI, |
| 68 | + MEDIA_BODY_PARAMETER_DEFAULT_VALUE, |
| 69 | + MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE, |
| 70 | + STACK_QUERY_PARAMETER_DEFAULT_VALUE, |
| 71 | + STACK_QUERY_PARAMETERS, |
| 72 | + V1_DISCOVERY_URI, V2_DISCOVERY_URI, |
| 73 | + APICoreVersionError, |
| 74 | + ResourceMethodParameters, |
| 75 | + _fix_up_media_path_base_url, |
| 76 | + _fix_up_media_upload, |
| 77 | + _fix_up_method_description, |
| 78 | + _fix_up_parameters, _urljoin, build, |
| 79 | + build_from_document, key2param) |
84 | 80 | from googleapiclient.discovery_cache import DISCOVERY_DOC_MAX_AGE |
85 | 81 | from googleapiclient.discovery_cache.base import Cache |
86 | | -from googleapiclient.errors import ( |
87 | | - HttpError, |
88 | | - InvalidJsonError, |
89 | | - MediaUploadSizeError, |
90 | | - ResumableUploadError, |
91 | | - UnacceptableMimeTypeError, |
92 | | - UnknownApiNameOrVersion, |
93 | | - UnknownFileType, |
94 | | -) |
95 | | -from googleapiclient.http import ( |
96 | | - HttpMock, |
97 | | - HttpMockSequence, |
98 | | - MediaFileUpload, |
99 | | - MediaIoBaseUpload, |
100 | | - MediaUpload, |
101 | | - MediaUploadProgress, |
102 | | - build_http, |
103 | | - tunnel_patch, |
104 | | -) |
| 82 | +from googleapiclient.errors import (HttpError, InvalidJsonError, |
| 83 | + MediaUploadSizeError, ResumableUploadError, |
| 84 | + UnacceptableMimeTypeError, |
| 85 | + UnknownApiNameOrVersion, UnknownFileType) |
| 86 | +from googleapiclient.http import (HttpMock, HttpMockSequence, MediaFileUpload, |
| 87 | + MediaIoBaseUpload, MediaUpload, |
| 88 | + MediaUploadProgress, build_http, |
| 89 | + tunnel_patch) |
105 | 90 | from googleapiclient.model import JsonModel |
106 | 91 | from googleapiclient.schema import Schemas |
107 | 92 |
|
@@ -156,6 +141,28 @@ def read_datafile(filename, mode="r"): |
156 | 141 | with open(datafile(filename), mode=mode) as f: |
157 | 142 | return f.read() |
158 | 143 |
|
| 144 | +def parse_version_to_tuple(version_string): |
| 145 | + """Safely converts a semantic version string to a comparable tuple of integers. |
| 146 | +
|
| 147 | + Example: "4.25.8" -> (4, 25, 8) |
| 148 | + Ignores non-numeric parts and handles common version formats. |
| 149 | +
|
| 150 | + Args: |
| 151 | + version_string: Version string in the format "x.y.z" or "x.y.z<suffix>" |
| 152 | +
|
| 153 | + Returns: |
| 154 | + Tuple of integers for the parsed version string. |
| 155 | + """ |
| 156 | + parts = [] |
| 157 | + for part in version_string.split("."): |
| 158 | + try: |
| 159 | + parts.append(int(part)) |
| 160 | + except ValueError: |
| 161 | + # If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here. |
| 162 | + # This is a simplification compared to 'packaging.parse_version', but sufficient |
| 163 | + # for comparing strictly numeric semantic versions. |
| 164 | + break |
| 165 | + return tuple(parts) |
159 | 166 |
|
160 | 167 | class SetupHttplib2(unittest.TestCase): |
161 | 168 | def test_retries(self): |
@@ -778,7 +785,19 @@ def test_self_signed_jwt_disabled(self): |
778 | 785 |
|
779 | 786 | REGULAR_ENDPOINT = "https://www.googleapis.com/plus/v1/" |
780 | 787 | MTLS_ENDPOINT = "https://www.mtls.googleapis.com/plus/v1/" |
781 | | - |
| 788 | +CONFIG_DATA_WITH_WORKLOAD = { |
| 789 | + "version": 1, |
| 790 | + "cert_configs": { |
| 791 | + "workload": { |
| 792 | + "cert_path": "path/to/cert/file", |
| 793 | + "key_path": "path/to/key/file", |
| 794 | + } |
| 795 | + }, |
| 796 | +} |
| 797 | +CONFIG_DATA_WITHOUT_WORKLOAD = { |
| 798 | + "version": 1, |
| 799 | + "cert_configs": {}, |
| 800 | +} |
782 | 801 |
|
783 | 802 | class DiscoveryFromDocumentMutualTLS(unittest.TestCase): |
784 | 803 | MOCK_CREDENTIALS = mock.Mock(spec=google.auth.credentials.Credentials) |
@@ -884,6 +903,55 @@ def test_mtls_with_provided_client_cert( |
884 | 903 | self.check_http_client_cert(plus, has_client_cert=use_client_cert) |
885 | 904 | self.assertEqual(plus._baseUrl, base_url) |
886 | 905 |
|
| 906 | + @parameterized.expand( |
| 907 | + [ |
| 908 | + ("never", "", CONFIG_DATA_WITH_WORKLOAD , REGULAR_ENDPOINT), |
| 909 | + ("auto", "", CONFIG_DATA_WITH_WORKLOAD, MTLS_ENDPOINT), |
| 910 | + ("always", "", CONFIG_DATA_WITH_WORKLOAD, MTLS_ENDPOINT), |
| 911 | + ("never", "", CONFIG_DATA_WITHOUT_WORKLOAD, REGULAR_ENDPOINT), |
| 912 | + ("auto", "", CONFIG_DATA_WITHOUT_WORKLOAD, REGULAR_ENDPOINT), |
| 913 | + ("always", "", CONFIG_DATA_WITHOUT_WORKLOAD, MTLS_ENDPOINT), |
| 914 | + ] |
| 915 | + ) |
| 916 | + @pytest.mark.skipif( |
| 917 | + parse_version_to_tuple(auth_version) < (2,43,0), |
| 918 | + reason="automatic mtls enablement when supported certs present only" |
| 919 | + "enabled in google-auth<=2.43.0" |
| 920 | + ) |
| 921 | + def test_mtls_with_provided_client_cert_unset_environment_variable( |
| 922 | + self, use_mtls_env, use_client_cert, config_data, base_url |
| 923 | + ): |
| 924 | + """Tests that mTLS is correctly handled when a client certificate is provided. |
| 925 | +
|
| 926 | + This test case verifies that when a client certificate is explicitly provided |
| 927 | + via `client_options` and GOOGLE_API_USE_CLIENT_CERTIFICATE is unset, the |
| 928 | + discovery document build process correctly configures the base URL for mTLS |
| 929 | + or regular endpoints based on the `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable. |
| 930 | + """ |
| 931 | + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): |
| 932 | + discovery = read_datafile("plus.json") |
| 933 | + config_filename = "mock_certificate_config.json" |
| 934 | + config_file_content = json.dumps(config_data) |
| 935 | + m = mock.mock_open(read_data=config_file_content) |
| 936 | + |
| 937 | + with mock.patch.dict( |
| 938 | + "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env} |
| 939 | + ): |
| 940 | + with mock.patch.dict( |
| 941 | + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert} |
| 942 | + ): |
| 943 | + with mock.patch("builtins.open", m): |
| 944 | + with mock.patch.dict("os.environ", {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename}): |
| 945 | + plus = build_from_document( |
| 946 | + discovery, |
| 947 | + credentials=self.MOCK_CREDENTIALS, |
| 948 | + client_options={ |
| 949 | + "client_encrypted_cert_source": self.client_encrypted_cert_source |
| 950 | + }, |
| 951 | + ) |
| 952 | + self.assertIsNotNone(plus) |
| 953 | + self.assertEqual(plus._baseUrl, base_url) |
| 954 | + |
887 | 955 | @parameterized.expand( |
888 | 956 | [ |
889 | 957 | ("never", "true"), |
@@ -961,6 +1029,71 @@ def test_mtls_with_default_client_cert( |
961 | 1029 | self.assertIsNotNone(plus) |
962 | 1030 | self.check_http_client_cert(plus, has_client_cert=use_client_cert) |
963 | 1031 | self.assertEqual(plus._baseUrl, base_url) |
| 1032 | + @parameterized.expand( |
| 1033 | + [ |
| 1034 | + ("never", "", CONFIG_DATA_WITH_WORKLOAD, REGULAR_ENDPOINT), |
| 1035 | + ("auto", "", CONFIG_DATA_WITH_WORKLOAD, MTLS_ENDPOINT), |
| 1036 | + ("always", "", CONFIG_DATA_WITH_WORKLOAD, MTLS_ENDPOINT), |
| 1037 | + ("never", "", CONFIG_DATA_WITHOUT_WORKLOAD, REGULAR_ENDPOINT), |
| 1038 | + ("auto", "", CONFIG_DATA_WITHOUT_WORKLOAD, REGULAR_ENDPOINT), |
| 1039 | + ("always", "", CONFIG_DATA_WITHOUT_WORKLOAD, MTLS_ENDPOINT), |
| 1040 | + ] |
| 1041 | + ) |
| 1042 | + @mock.patch( |
| 1043 | + "google.auth.transport.mtls.has_default_client_cert_source", autospec=True |
| 1044 | + ) |
| 1045 | + @mock.patch( |
| 1046 | + "google.auth.transport.mtls.default_client_encrypted_cert_source", autospec=True |
| 1047 | + ) |
| 1048 | + @pytest.mark.skipif( |
| 1049 | + parse_version_to_tuple(auth_version) < (2,43,0), |
| 1050 | + reason="automatic mtls enablement when supported certs present only" |
| 1051 | + "enabled in google-auth<=2.43.0" |
| 1052 | + ) |
| 1053 | + def test_mtls_with_default_client_cert_with_unset_environment_variable( |
| 1054 | + self, |
| 1055 | + use_mtls_env, |
| 1056 | + use_client_cert, |
| 1057 | + config_data, |
| 1058 | + base_url, |
| 1059 | + default_client_encrypted_cert_source, |
| 1060 | + has_default_client_cert_source, |
| 1061 | + ): |
| 1062 | + """Tests mTLS handling when falling back to a default client certificate. |
| 1063 | +
|
| 1064 | + This test simulates the scenario where no client certificate is explicitly |
| 1065 | + provided, and the library successfully finds and uses a default client |
| 1066 | + certificate when GOOGLE_API_USE_CLIENT_CERTIFICATE is unset. It mocks the |
| 1067 | + default certificate discovery process and checks that the base URL is |
| 1068 | + correctly set for mTLS or regular endpoints depending on the |
| 1069 | + `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable. |
| 1070 | + """ |
| 1071 | + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): |
| 1072 | + has_default_client_cert_source.return_value = True |
| 1073 | + default_client_encrypted_cert_source.return_value = ( |
| 1074 | + self.client_encrypted_cert_source |
| 1075 | + ) |
| 1076 | + discovery = read_datafile("plus.json") |
| 1077 | + config_filename = "mock_certificate_config.json" |
| 1078 | + config_file_content = json.dumps(config_data) |
| 1079 | + m = mock.mock_open(read_data=config_file_content) |
| 1080 | + |
| 1081 | + with mock.patch.dict( |
| 1082 | + "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env} |
| 1083 | + ): |
| 1084 | + with mock.patch.dict( |
| 1085 | + "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert} |
| 1086 | + ): |
| 1087 | + with mock.patch("builtins.open", m): |
| 1088 | + with mock.patch.dict("os.environ", {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename}): |
| 1089 | + plus = build_from_document( |
| 1090 | + discovery, |
| 1091 | + credentials=self.MOCK_CREDENTIALS, |
| 1092 | + adc_cert_path=self.ADC_CERT_PATH, |
| 1093 | + adc_key_path=self.ADC_KEY_PATH, |
| 1094 | + ) |
| 1095 | + self.assertIsNotNone(plus) |
| 1096 | + self.assertEqual(plus._baseUrl, base_url) |
964 | 1097 |
|
965 | 1098 | @parameterized.expand( |
966 | 1099 | [ |
|
0 commit comments