Repository: demisto/content
Pull request #39803: Google Apigee Integration - Error in API call
Author: login: BarGali
Labels: name: docs-approved
name: ready-for-pipeline-running
description: Whether the pr is ready for running the whole pipeline, including testing on SAAS machines
Created: 2025-05-04T17:18:24Z | Merged: 2025-05-05T11:47:17Z

## Status
- [ ] In Progress
- [x] Ready
- [ ] In Hold - (Reason for hold)

## Related Issues
ticket: https://jira-dc.paloaltonetworks.com/browse/XSUP-50058

## Description
On token refresh failure, regenerate a new access token

## Must have
- [x] Tests
- [x] Documentation 



Diff:
diff --git a/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py b/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
index 49b333ee0293..a4e9fe198876 100644
--- a/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
+++ b/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
@@ -77,6 +77,7 @@ def get_access_token(self) -> str:
             token_expiration_seconds=float(token_expiration_seconds),
             current_time=current_time
         ):
+            demisto.debug('existing access token is valid, no need to regenerate token')
             return access_token
         # There's no token or it is expired
         access_token, token_expiration_seconds, refresh_token = self.get_token_request(refresh_token)
@@ -87,7 +88,7 @@ def get_access_token(self) -> str:
             REFRESH_TOKEN_STR: refresh_token,
         }
         set_integration_context(context=integration_context)
-        demisto.debug('successfully updated access token')
+        demisto.info('successfully updated access token')
 
         return access_token
 
@@ -98,15 +99,7 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
        Returns:
            tuple[str, str]: token and its expiration date
         """
-        grant_type = REFRESH_TOKEN_STR if refresh_token else 'password'
-        data = {
-            'grant_type': grant_type
-        }
-        if refresh_token:
-            data[REFRESH_TOKEN_STR] = refresh_token
-        else:
-            data['username'] = self.username
-            data['password'] = self.password
+        data = Client.generate_data_with_refresh_token(refresh_token) if refresh_token else self.generate_data_with_username()
         # The "Authorization" token is a hard-coded value that the API requires in the header.
         # https://docs.apigee.com/api-platform/system-administration/management-api-tokens
         headers = {
@@ -116,9 +109,34 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            if refresh_token and 'Invalid refresh token' in str(e):
+                demisto.debug(f'Failed to generate access token using refresh token. Attempting to generate a new access token \
+                    using username and password. Original error is: {str(e)}')
+                data = self.generate_data_with_username()
+                token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+            else:
+                raise e
         return token_response.get(ACCESS_TOKEN_STR), token_response.get('expires_in'), token_response.get(REFRESH_TOKEN_STR)
 
+    @staticmethod
+    def generate_data_with_refresh_token(refresh_token: str) -> dict:
+        demisto.debug('generates a new access token based on a refresh token.')
+        return {
+            'grant_type': REFRESH_TOKEN_STR,
+            REFRESH_TOKEN_STR: refresh_token
+        }
+    
+    def generate_data_with_username(self) -> dict:
+        demisto.debug('generates a new access token based on a username and password.')
+        return {
+            'grant_type': 'password',
+            'username': self.username,
+            'password': self.password,
+        }
+    
     @staticmethod
     def is_token_valid(token_initiate_time: float, token_expiration_seconds: float, current_time: float) -> bool:
         """
diff --git a/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.yml b/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.yml
index 76ab24951deb..01e61c077f1b 100644
--- a/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.yml
+++ b/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.yml
@@ -72,7 +72,7 @@ script:
       required: false
     description: Gets logs from Google Apigee.
     name: google-apigee-get-events
-  dockerimage: demisto/python3:3.12.8.1983910
+  dockerimage: demisto/python3:3.12.8.3296088
   isfetchevents: true
   runonce: false
   script: '-'
diff --git a/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector_test.py b/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector_test.py
index 7090aafdc7e6..fbf30ca62460 100644
--- a/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector_test.py
+++ b/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector_test.py
@@ -11,6 +11,8 @@ def mock_client(mocker):
     client = Client(base_url='https://test.com', verify=False, proxy=False, org_name='org', username='user',
                     password='password', zone='zone')
     mocker.patch.object(Client, 'get_access_token', return_value={'access_token': 'access_token'})
+    mocker.patch.object(Client, '_http_request', return_value={'access_token': 'access_token', 'expires_in': 3600,
+                                                               'refresh_token': 'refresh_token'})
     return client
 
 
@@ -187,3 +189,92 @@ def test_test_module(requests_mock, mocker):
 def test_is_token_valid(mocker, scenario, token_initiate_time, token_expiration_seconds, current_time, result):
     is_token_valid = Client.is_token_valid(token_initiate_time, token_expiration_seconds, current_time)
     assert is_token_valid == result, f'{scenario} - does not match expected value'
+
+def test_get_token_with_username(mocker):
+    """
+    Given:
+        - A mock client configured with username and password credentials.
+    When:
+        - Calling `get_token_request` to retrieve an access token.
+    Then:
+        - Ensure the HTTP request is made with the correct method, URL, headers, and body.
+        - Verify the returned access_token, expires_in, and refresh_token values.
+    """
+    client = mock_client(mocker)
+    spy_http = mocker.spy(Client, '_http_request')
+    access_token, expires_in, refresh_token = client.get_token_request()
+    assert access_token == 'access_token'
+    assert expires_in == 3600
+    assert refresh_token == 'refresh_token'
+    args = spy_http.call_args
+    assert args[0] == ('POST',)
+    assert args[1] == {'full_url': 'https://zone.login.apigee.com/oauth/token',
+                       'url_suffix': '/oauth/token',
+                       'data': {'grant_type': 'password', 'username': 'user', 'password': 'password'},
+                       'headers': {'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
+                                   'Accept': 'application/json;charset=utf-8',
+                                   'Authorization': 'Basic ZWRnZWNsaTplZGdlY2xpc2VjcmV0'}}
+    
+def test_get_token_with_valid_refresh_token(mocker):
+    """
+    Given:
+        - A mock client configured to use a refresh token.
+    When:
+        - Calling `get_token_request` with a valid refresh token.
+    Then:
+        - Ensure the HTTP request is sent as a POST to the correct URL.
+        - Confirm the payload includes the proper grant type and refresh token.
+        - Verify the expected access_token, expires_in, and refresh_token are returned.
+    """
+    client = mock_client(mocker)
+    spy_http = mocker.spy(Client, '_http_request')
+    access_token, expires_in, refresh_token = client.get_token_request(refresh_token = 'refresh_token')
+    assert access_token == 'access_token'
+    assert expires_in == 3600
+    assert refresh_token == 'refresh_token'
+    
+    args = spy_http.call_args
+    assert args[0] == ('POST',)
+    assert args[1] == {'full_url': 'https://zone.login.apigee.com/oauth/token',
+                       'url_suffix': '/oauth/token',
+                       'data': {'grant_type': 'refresh_token', 'refresh_token': 'refresh_token'},
+                       'headers': {'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
+                                   'Accept': 'application/json;charset=utf-8',
+                                   'Authorization': 'Basic ZWRnZWNsaTplZGdlY2xpc2VjcmV0'}}
+
+def test_get_token_with_invalid_refresh_token(mocker):
+    """
+    Given:
+        - A mock client configured to use a refresh token.
+    When:
+        - Calling `get_token_request` with an invalid refresh token that triggers an exception.
+    Then:
+        - The client should retry the request using the password grant type.
+        - Ensure the first HTTP request uses the refresh token grant type with the correct payload.
+        - Ensure the second HTTP request uses the password grant type with the correct credentials.
+        - Verify that the final response includes the expected access_token, expires_in, and refresh_token.
+    """
+    client = mock_client(mocker)
+    http_mock = mocker.patch.object(Client, '_http_request', side_effect=[Exception('Invalid refresh token'),
+                                                                          {'access_token': 'access_token', 'expires_in': 3600,
+                                                                           'refresh_token': 'refresh_token'}])
+    access_token, expires_in, refresh_token = client.get_token_request(refresh_token='refresh_token')
+    
+    assert http_mock.call_count == 2
+    first_call = http_mock.call_args_list[0]
+    second_call = http_mock.call_args_list[1]
+    
+    # Inspecting the first call
+    assert first_call[0] == ('POST',)
+    assert first_call[1]['data']['grant_type'] == 'refresh_token'
+    assert first_call[1]['data']['refresh_token'] == 'refresh_token'
+
+    # Inspecting the second call
+    assert second_call[0] == ('POST',)
+    assert second_call[1]['data']['grant_type'] == 'password'
+    assert second_call[1]['data']['username'] == 'user'
+    assert second_call[1]['data']['password'] == 'password'
+    
+    assert access_token == 'access_token'
+    assert expires_in == 3600
+    assert refresh_token == 'refresh_token'
\ No newline at end of file
diff --git a/Packs/GoogleApigee/ReleaseNotes/1_0_2.md b/Packs/GoogleApigee/ReleaseNotes/1_0_2.md
new file mode 100644
index 000000000000..fb298334b63d
--- /dev/null
+++ b/Packs/GoogleApigee/ReleaseNotes/1_0_2.md
@@ -0,0 +1,8 @@
+
+#### Integrations
+
+##### Google Apigee
+- Updated the Docker image to: *demisto/python3:3.12.8.3296088*.
+
+Improved implementation of the access token generation.
+  When the refresh token is invalid, a new access token will be generated using the username and password.
diff --git a/Packs/GoogleApigee/pack_metadata.json b/Packs/GoogleApigee/pack_metadata.json
index 125109f675d8..976cdd266655 100644
--- a/Packs/GoogleApigee/pack_metadata.json
+++ b/Packs/GoogleApigee/pack_metadata.json
@@ -2,7 +2,7 @@
     "name": "Google Apigee Edge",
     "description": "Apigee is Google Cloud's native API management platform that can be used to build, manage, and secure APIs — for any use case, environment, or scale. Apigee offers high performance API proxies to create a consistent, reliable interface for your backend services. The proxy layer gives you granular control over security, rate limiting, quotas, analytics, and more for all of your services.\nApigee supports REST, gRPC, SOAP, and GraphQL, providing the flexibility to implement any API architectural style.",
     "support": "xsoar",
-    "currentVersion": "1.0.1",
+    "currentVersion": "1.0.2",
     "author": "Cortex XSOAR",
     "url": "https://www.paloaltonetworks.com/cortex",
     "email": "",


Reviews:
user: login: ShacharKidor
body: @BarGali - Good work!
Added a few comments, please have a look. 
Also, let's test the error handling on an XSIAM machine before merging. 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T18:47:44Z
user: login: yuvalbenshalom
body: see my comment

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T19:00:44Z
user: login: ShacharKidor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T19:22:43Z
user: login: ShirleyDenkberg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T05:57:50Z
user: login: BarGali
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T07:13:39Z
user: login: BarGali
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T07:15:49Z
user: login: BarGali
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T07:17:11Z
user: login: ShacharKidor
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T08:39:52Z
user: login: ShacharKidor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T08:40:28Z
user: login: ShacharKidor
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T10:07:20Z

Review comments:
diff_hunk: @@ -98,15 +99,7 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
        Returns:
            tuple[str, str]: token and its expiration date
         """
-        grant_type = REFRESH_TOKEN_STR if refresh_token else 'password'
-        data = {
-            'grant_type': grant_type
-        }
-        if refresh_token:
-            data[REFRESH_TOKEN_STR] = refresh_token
-        else:
-            data['username'] = self.username
-            data['password'] = self.password
+        data = Client.generte_data_with_refresh_token(refresh_token) if refresh_token else self.generte_data_with_username()
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: ShacharKidor
body: why do we use here client and not self? 
created_at: 2025-05-04T18:19:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,32 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: ShacharKidor
body: Please add. debug log here explaining what happened and how we handled the case. 
Include also the original error that was caught. 
created_at: 2025-05-04T18:21:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,32 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            if refresh_token and 'Invalid refresh token' in str(e):
+                data = self.generte_data_with_username()
+                token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+            else:
+                raise e
         return token_response.get(ACCESS_TOKEN_STR), token_response.get('expires_in'), token_response.get(REFRESH_TOKEN_STR)
 
+    @staticmethod
+    def generte_data_with_refresh_token(refresh_token: str) -> dict:
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: ShacharKidor
body: Please fix the typo 'generte' with 'generate' in all relevant places. 
```suggestion
    def generate_data_with_refresh_token(refresh_token: str) -> dict:
```
created_at: 2025-05-04T18:23:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,32 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            if refresh_token and 'Invalid refresh token' in str(e):
+                data = self.generte_data_with_username()
+                token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+            else:
+                raise e
         return token_response.get(ACCESS_TOKEN_STR), token_response.get('expires_in'), token_response.get(REFRESH_TOKEN_STR)
 
+    @staticmethod
+    def generte_data_with_refresh_token(refresh_token: str) -> dict:
+        demisto.debug('generte new token with refresh token')
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: ShacharKidor
body: ```suggestion
        demisto.debug('generates a new access token based on a refresh token.')
```
created_at: 2025-05-04T18:23:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,32 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            if refresh_token and 'Invalid refresh token' in str(e):
+                data = self.generte_data_with_username()
+                token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+            else:
+                raise e
         return token_response.get(ACCESS_TOKEN_STR), token_response.get('expires_in'), token_response.get(REFRESH_TOKEN_STR)
 
+    @staticmethod
+    def generte_data_with_refresh_token(refresh_token: str) -> dict:
+        demisto.debug('generte new token with refresh token')
+        return {
+            'grant_type': REFRESH_TOKEN_STR,
+            REFRESH_TOKEN_STR: refresh_token
+        }
+    
+    def generte_data_with_username(self) -> dict:
+        demisto.debug('generte new token with user name')
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: ShacharKidor
body: ```suggestion
        demisto.debug('generates a new access token based on a username and password.')
```
created_at: 2025-05-04T18:24:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,32 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            if refresh_token and 'Invalid refresh token' in str(e):
+                data = self.generte_data_with_username()
+                token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+            else:
+                raise e
         return token_response.get(ACCESS_TOKEN_STR), token_response.get('expires_in'), token_response.get(REFRESH_TOKEN_STR)
 
+    @staticmethod
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: ShacharKidor
body: Why is the first one a static method and the second isn't? Is it on purpose? 
created_at: 2025-05-04T18:27:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -187,3 +193,48 @@ def test_test_module(requests_mock, mocker):
 def test_is_token_valid(mocker, scenario, token_initiate_time, token_expiration_seconds, current_time, result):
     is_token_valid = Client.is_token_valid(token_initiate_time, token_expiration_seconds, current_time)
     assert is_token_valid == result, f'{scenario} - does not match expected value'
+
+def test_get_token_with_username(mocker):
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector_test.py
user: login: ShacharKidor
body: Please add a docstring for each Unit Test - in the structure of Given, When Then. 
created_at: 2025-05-04T18:34:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -187,3 +193,48 @@ def test_test_module(requests_mock, mocker):
 def test_is_token_valid(mocker, scenario, token_initiate_time, token_expiration_seconds, current_time, result):
     is_token_valid = Client.is_token_valid(token_initiate_time, token_expiration_seconds, current_time)
     assert is_token_valid == result, f'{scenario} - does not match expected value'
+
+def test_get_token_with_username(mocker):
+    client = mock_client(mocker)
+    spy_http = mocker.spy(Client, '_http_request')
+    access_token, expires_in, refresh_token = client.get_token_request()
+    assert access_token == 'access_token'
+    assert expires_in == 3600
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector_test.py
user: login: ShacharKidor
body: Why do we need the first three assertions? 
relevant to this test and to the next one. 
created_at: 2025-05-04T18:45:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -187,3 +193,48 @@ def test_test_module(requests_mock, mocker):
 def test_is_token_valid(mocker, scenario, token_initiate_time, token_expiration_seconds, current_time, result):
     is_token_valid = Client.is_token_valid(token_initiate_time, token_expiration_seconds, current_time)
     assert is_token_valid == result, f'{scenario} - does not match expected value'
+
+def test_get_token_with_username(mocker):
+    client = mock_client(mocker)
+    spy_http = mocker.spy(Client, '_http_request')
+    access_token, expires_in, refresh_token = client.get_token_request()
+    assert access_token == 'access_token'
+    assert expires_in == 3600
+    assert refresh_token == 'refresh_token'
+    args = spy_http.call_args
+    assert args[0] == ('POST',)
+    assert args[1] == {'full_url': 'https://zone.login.apigee.com/oauth/token',
+                       'url_suffix': '/oauth/token',
+                       'data': {'grant_type': 'password', 'username': 'user', 'password': 'password'},
+                       'headers': {'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
+                                   'Accept': 'application/json;charset=utf-8',
+                                   'Authorization': 'Basic ZWRnZWNsaTplZGdlY2xpc2VjcmV0'}}
+    
+def test_get_token_with_valid_refresh_token(mocker):
+    client = mock_client(mocker)
+    spy_http = mocker.spy(Client, '_http_request')
+    access_token, expires_in, refresh_token = client.get_token_request(refresh_token = 'token')
+    assert access_token == 'access_token'
+    assert expires_in == 3600
+    assert refresh_token == 'refresh_token'
+    
+    args = spy_http.call_args
+    assert args[0] == ('POST',)
+    assert args[1] == {'full_url': 'https://zone.login.apigee.com/oauth/token',
+                       'url_suffix': '/oauth/token',
+                       'data': {'grant_type': 'refresh_token', 'refresh_token': 'valid'},
+                       'headers': {'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
+                                   'Accept': 'application/json;charset=utf-8',
+                                   'Authorization': 'Basic ZWRnZWNsaTplZGdlY2xpc2VjcmV0'}}
+
+def test_get_token_with_invalid_refresh_token(mocker):
+    client = mock_client(mocker)
+    
+    http_mock = mocker.patch.object(Client, '_http_request', side_effect=[Exception('Invalid refresh token'),
+                                                                          {'access_token': 'access_token', 'expires_in': 3600,
+                                                                           'refresh_token': 'refresh_token'}])
+    access_token, expires_in, refresh_token = client.get_token_request(refresh_token='refresh')
+    assert access_token == 'access_token'
+    assert expires_in == 3600
+    assert refresh_token == 'refresh_token'
+    assert http_mock.call_count == 2
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector_test.py
user: login: ShacharKidor
body: So what do we test here eventually? Can you please explain? 
created_at: 2025-05-04T18:46:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,32 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            if refresh_token and 'Invalid refresh token' in str(e):
+                data = self.generte_data_with_username()
+                token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: yuvalbenshalom
body: If i understand correctly, we shold create a new refresh token and save it in this case.
created_at: 2025-05-04T18:56:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,32 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            if refresh_token and 'Invalid refresh token' in str(e):
+                data = self.generte_data_with_username()
+                token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: ShacharKidor
body: @yuvalbenshalom yes. You extract the new access token, expiration time, and refresh token in line 120, and save them in the integration context - in the caller function. 
@BarGali - correct me if I am wrong here. 
created_at: 2025-05-04T19:22:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,7 @@
+
+#### Integrations
+
+##### Google Apigee
+
+- Improved implementation of the access token generation.
path: Packs/GoogleApigee/ReleaseNotes/1_0_2.md
user: login: ShirleyDenkberg
body: ```suggestion
Improved implementation of the access token generation.
```
created_at: 2025-05-05T05:57:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -98,15 +99,7 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
        Returns:
            tuple[str, str]: token and its expiration date
         """
-        grant_type = REFRESH_TOKEN_STR if refresh_token else 'password'
-        data = {
-            'grant_type': grant_type
-        }
-        if refresh_token:
-            data[REFRESH_TOKEN_STR] = refresh_token
-        else:
-            data['username'] = self.username
-            data['password'] = self.password
+        data = Client.generte_data_with_refresh_token(refresh_token) if refresh_token else self.generte_data_with_username()
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: BarGali
body: generte_data_with_refresh_token is staticmethod of the class (it's not requires any of the instance params)
created_at: 2025-05-05T07:13:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,32 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            if refresh_token and 'Invalid refresh token' in str(e):
+                data = self.generte_data_with_username()
+                token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+            else:
+                raise e
         return token_response.get(ACCESS_TOKEN_STR), token_response.get('expires_in'), token_response.get(REFRESH_TOKEN_STR)
 
+    @staticmethod
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: BarGali
body: the other one uses self and this one is not
created_at: 2025-05-05T07:15:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,32 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            if refresh_token and 'Invalid refresh token' in str(e):
+                data = self.generte_data_with_username()
+                token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: BarGali
body: @ShacharKidor is right
created_at: 2025-05-05T07:17:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,35 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            demisto.debug(f'Error occured: {e}')
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: ShacharKidor
body: ```suggestion
```
created_at: 2025-05-05T08:31:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,35 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            demisto.debug(f'Error occured: {e}')
+            if refresh_token and 'Invalid refresh token' in str(e):
+                demisto.debug('Failed to generate access token using refresh token.'
+                              'Attempting to generate a new access token using username and password.')
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: ShacharKidor
body: ```suggestion
                demisto.debug(f'Failed to generate access token using refresh token.'
                              'Attempting to generate a new access token using username and password. Original error is: {str(e)}')
```
created_at: 2025-05-05T08:32:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,35 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            demisto.debug(f'Error occured: {e}')
+            if refresh_token and 'Invalid refresh token' in str(e):
+                demisto.debug('Failed to generate access token using refresh token.'
+                              'Attempting to generate a new access token using username and password.')
+                data = self.generte_data_with_username()
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: ShacharKidor
body: ```suggestion
                data = self.generate_data_with_username()
```
created_at: 2025-05-05T08:33:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,9 +109,35 @@ def get_token_request(self, refresh_token: str = '') -> tuple[str, str, str]:
         }
         zone = f'{self.zone}.' if self.zone else ''
         url = f'https://{zone}login.apigee.com/oauth/token'
-        token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        try:
+            token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+        except Exception as e:
+            demisto.debug(f'Error occured: {e}')
+            if refresh_token and 'Invalid refresh token' in str(e):
+                demisto.debug('Failed to generate access token using refresh token.'
+                              'Attempting to generate a new access token using username and password.')
+                data = self.generte_data_with_username()
+                token_response = self._http_request('POST', full_url=url, url_suffix='/oauth/token', data=data, headers=headers)
+            else:
+                raise e
         return token_response.get(ACCESS_TOKEN_STR), token_response.get('expires_in'), token_response.get(REFRESH_TOKEN_STR)
 
+    @staticmethod
+    def generate_data_with_refresh_token(refresh_token: str) -> dict:
+        demisto.debug('generates a new access token based on a refresh token.')
+        return {
+            'grant_type': REFRESH_TOKEN_STR,
+            REFRESH_TOKEN_STR: refresh_token
+        }
+    
+    def generte_data_with_username(self) -> dict:
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py
user: login: ShacharKidor
body: ```suggestion
    def generate_data_with_username(self) -> dict:
```
created_at: 2025-05-05T08:33:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -187,3 +193,92 @@ def test_test_module(requests_mock, mocker):
 def test_is_token_valid(mocker, scenario, token_initiate_time, token_expiration_seconds, current_time, result):
     is_token_valid = Client.is_token_valid(token_initiate_time, token_expiration_seconds, current_time)
     assert is_token_valid == result, f'{scenario} - does not match expected value'
+
+def test_get_token_with_username(mocker):
+    """
+    Given:
+        - A mock client configured with username and password credentials.
+    When:
+        - Calling `get_token_request` to retrieve an access token.
+    Then:
+        - Ensure the HTTP request is made with the correct method, URL, headers, and body.
+        - Verify the returned access_token, expires_in, and refresh_token values.
+    """
+    client = mock_client(mocker)
+    spy_http = mocker.spy(Client, '_http_request')
+    access_token, expires_in, refresh_token = client.get_token_request()
+    assert access_token == 'access_token'
+    assert expires_in == 3600
+    assert refresh_token == 'refresh_token'
+    args = spy_http.call_args
+    assert args[0] == ('POST',)
+    assert args[1] == {'full_url': 'https://zone.login.apigee.com/oauth/token',
+                       'url_suffix': '/oauth/token',
+                       'data': {'grant_type': 'password', 'username': 'user', 'password': 'password'},
+                       'headers': {'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
+                                   'Accept': 'application/json;charset=utf-8',
+                                   'Authorization': 'Basic ZWRnZWNsaTplZGdlY2xpc2VjcmV0'}}
+    
+def test_get_token_with_valid_refresh_token(mocker):
+    """
+    Given:
+        - A mock client configured to use a refresh token.
+    When:
+        - Calling `get_token_request` with a valid refresh token.
+    Then:
+        - Ensure the HTTP request is sent as a POST to the correct URL.
+        - Confirm the payload includes the proper grant type and refresh token.
+        - Verify the expected access_token, expires_in, and refresh_token are returned.
+    """
+    client = mock_client(mocker)
+    spy_http = mocker.spy(Client, '_http_request')
+    access_token, expires_in, refresh_token = client.get_token_request(refresh_token = 'token')
+    assert access_token == 'access_token'
+    assert expires_in == 3600
+    assert refresh_token == 'refresh_token'
+    
+    args = spy_http.call_args
+    assert args[0] == ('POST',)
+    assert args[1] == {'full_url': 'https://zone.login.apigee.com/oauth/token',
+                       'url_suffix': '/oauth/token',
+                       'data': {'grant_type': 'refresh_token', 'refresh_token': 'valid'},
+                       'headers': {'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
+                                   'Accept': 'application/json;charset=utf-8',
+                                   'Authorization': 'Basic ZWRnZWNsaTplZGdlY2xpc2VjcmV0'}}
+
+def test_get_token_with_invalid_refresh_token(mocker):
+    """
+    Given:
+        - A mock client configured to use a refresh token.
+    When:
+        - Calling `get_token_request` with an invalid refresh token that triggers an exception.
+    Then:
+        - The client should retry the request using the password grant type.
+        - Ensure the first HTTP request uses the refresh token grant type with the correct payload.
+        - Ensure the second HTTP request uses the password grant type with the correct credentials.
+        - Verify that the final response includes the expected access_token, expires_in, and refresh_token.
+    """
+    client = mock_client(mocker)
+    http_mock = mocker.patch.object(Client, '_http_request', side_effect=[Exception('Invalid refresh token'),
+                                                                          {'access_token': 'access_token', 'expires_in': 3600,
+                                                                           'refresh_token': 'refresh_token'}])
+    access_token, expires_in, refresh_token = client.get_token_request(refresh_token='invalid')
+    
+    assert http_mock.call_count == 2
+    first_call = http_mock.call_args_list[0]
+    second_call = http_mock.call_args_list[1]
+    
+    # Inspecting the first call
+    assert first_call[0] == ('POST',)
+    assert first_call[1]['data']['grant_type'] == 'refresh_token'
+    assert first_call[1]['data']['refresh_token'] == 'valid'
path: Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector_test.py
user: login: ShacharKidor
body: valid? 
created_at: 2025-05-05T08:40:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: guardrails[bot]
created_at: 2025-05-04T17:20:45Z
body: 
All previously detected findings have been fixed. Good job! 👍🎉

We will keep this comment up-to-date as you go along and notify you of any security issues that we identify.


---

👉 Go to the [dashboard](https://dashboard.guardrails.io/gh/demisto/repos/22387/scans?sha=d4a41860fde5ebece4340739e32c097a39ea735d) for detailed results.

📥 Happy? Share your [feedback](https://guardrails.typeform.com/to/BCgBkB) with us.

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-04T17:25:14Z
body: <!-- Pytest Coverage Comment: pre-commit -->
<a href="https://github.com/demisto/content/blob/d4a41860fde5ebece4340739e32c097a39ea735d/README.md"><img alt="Coverage" src="https://img.shields.io/badge/Coverage-77%25-yellow.svg" /></a><details><summary>Coverage Report </summary><table><tr><th>File</th><th>Stmts</th><th>Miss</th><th>Cover</th><th>Missing</th></tr><tbody><tr><td colspan="5"><b>Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/demisto/content/blob/d4a41860fde5ebece4340739e32c097a39ea735d/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py">GoogleApigeeEventCollector.py</a></td><td>127</td><td>28</td><td>77%</td><td><a href="https://github.com/demisto/content/blob/d4a41860fde5ebece4340739e32c097a39ea735d/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py#L68-L73">68&ndash;73</a>, <a href="https://github.com/demisto/content/blob/d4a41860fde5ebece4340739e32c097a39ea735d/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py#L75">75</a>, <a href="https://github.com/demisto/content/blob/d4a41860fde5ebece4340739e32c097a39ea735d/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py#L80-L81">80&ndash;81</a>, <a href="https://github.com/demisto/content/blob/d4a41860fde5ebece4340739e32c097a39ea735d/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py#L83-L84">83&ndash;84</a>, <a href="https://github.com/demisto/content/blob/d4a41860fde5ebece4340739e32c097a39ea735d/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py#L90-L91">90&ndash;91</a>, <a href="https://github.com/demisto/content/blob/d4a41860fde5ebece4340739e32c097a39ea735d/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py#L93">93</a>, <a href="https://github.com/demisto/content/blob/d4a41860fde5ebece4340739e32c097a39ea735d/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py#L121">121</a>, <a href="https://github.com/demisto/content/blob/d4a41860fde5ebece4340739e32c097a39ea735d/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py#L263-L270">263&ndash;270</a>, <a href="https://github.com/demisto/content/blob/d4a41860fde5ebece4340739e32c097a39ea735d/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py#L297">297</a>, <a href="https://github.com/demisto/content/blob/d4a41860fde5ebece4340739e32c097a39ea735d/Packs/GoogleApigee/Integrations/GoogleApigeeEventCollector/GoogleApigeeEventCollector.py#L326-L329">326&ndash;329</a></td></tr><tr><td><b>TOTAL</b></td><td><b>127</b></td><td><b>28</b></td><td><b>77%</b></td><td>&nbsp;</td></tr></tbody></table></details>

| Tests | Skipped | Failures | Errors | Time |
| ----- | ------- | -------- | -------- | ------------------ |
| 14 | 0 :zzz: | 0 :x: | 0 :fire: | 3.341s :stopwatch: |

author_association: NONE
user: login: content-bot
created_at: 2025-05-04T18:40:43Z
body: ### This PR was automatically updated by a [GitHub Action](https://github.com/demisto/content/actions/runs/14824088415)
- **Core** pack version was bumped to **3.2.41**.

To stop automatic version bumps, add the `ignore-auto-bump-version` label to the github PR.

author_association: CONTRIBUTOR
user: login: ShacharKidor
created_at: 2025-05-04T18:55:53Z
body: Hi @ShirleyDenkberg, can you please review the release note file? 

Thank you in advance. 
author_association: CONTRIBUTOR
user: login: ShirleyDenkberg
created_at: 2025-05-05T05:58:41Z
body: @yuvalbenshalom @ShacharKidor Doc review completed.
author_association: CONTRIBUTOR
user: login: content-bot
created_at: 2025-05-05T10:33:55Z
body: Validate summary

Verdict: PR can be force merged from validate perspective? ✅
author_association: CONTRIBUTOR