From 7504b420075bdf575714efde42f2abca696d1ec7 Mon Sep 17 00:00:00 2001 From: Guillaume Charest <1690085+gcharest@users.noreply.github.com> Date: Fri, 2 Feb 2024 11:58:30 -0500 Subject: [PATCH] Refactor/google drive module (#392) * fix: render commands available in dev only * fix: move google-service command to bottom * feat: make test command available in dev only * fix: fix mypy path for current project structure * fix: update pylint cwd * fix: ignore google.oauth2 missing stub warning * fix: config flake8 instead of pylint * feat: add a decorator to handle api calls errors * feat: refactor basic drive functions * feat: add get_file_by_name function * feat: add list metadata function * feat: use new drive module functions * fix: shorten test command * fix: handle invalid folder * feat: modify wrapper to provide context * fix: add missing parameter to list_metadata * fix: surround function name w/ single quotes * fix: google drive healthcheck * fix: matching error message * feat: add healtcheck message * fix: update module docstrings w/ more details * fix: remove import from google_service * fix: update module docstring * feat: add basic docstring for Docs module * fix: replicate current tests for conformity * fix: add loop to handle pagination Co-authored-by: Pat Heard * fix: ensure all files are returned * fix: add test to handle pagination * fix: log errors * fix: remove unused variables * feat: include ValueError logging in the wrapper function * fix: adjust tests to handle logging errors * fix: test names * fix: lint --------- Co-authored-by: Pat Heard --- .devcontainer/devcontainer.json | 1 - .vscode/settings.json | 6 + app/commands/google_service.py | 37 +- app/commands/sre.py | 10 +- .../google_workspace/google_docs.py | 4 + .../google_workspace/google_drive.py | 339 ++++++++++++++++++ .../google_workspace/google_meet.py | 9 +- .../google_workspace/google_service.py | 47 ++- app/main.py | 9 +- app/mypy.ini | 2 + .../test_google_drive_module.py | 155 ++++++++ .../google_workspace/test_google_service.py | 75 +++- 12 files changed, 650 insertions(+), 44 deletions(-) create mode 100644 app/integrations/google_workspace/google_docs.py create mode 100644 app/integrations/google_workspace/google_drive.py create mode 100644 app/mypy.ini create mode 100644 app/tests/integrations/google_workspace/test_google_drive_module.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 88a47015..bda12a20 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -11,7 +11,6 @@ "ms-python.python", "ms-python.vscode-pylance", "ms-python.flake8", - "ms-python.pylint", "ms-python.mypy-type-checker", "mtxr.sqltools", "mtxr.sqltools-driver-pg", diff --git a/.vscode/settings.json b/.vscode/settings.json index 6e4f5b5d..70e39139 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,9 @@ { "python.pylintPath": "/usr/local/py-utils/bin/pylint", + "mypy-type-checker.cwd": "${workspaceFolder}/app", + "pylint.cwd": "${workspaceFolder}/app", + "pylint.path": [ + "[\"../../usr/local/py-utils/bin/pylint\"]" + ], + "flake8.cwd": "${workspaceFolder}/app" } \ No newline at end of file diff --git a/app/commands/google_service.py b/app/commands/google_service.py index a0609758..21e0ca53 100644 --- a/app/commands/google_service.py +++ b/app/commands/google_service.py @@ -1,17 +1,19 @@ """Testing new google service (will be removed)""" import os -from integrations.google_workspace.google_service import get_google_service +from integrations.google_workspace import google_drive from dotenv import load_dotenv load_dotenv() SRE_DRIVE_ID = os.environ.get("SRE_DRIVE_ID") SRE_INCIDENT_FOLDER = os.environ.get("SRE_INCIDENT_FOLDER") +INCIDENT_TEMPLATE = os.environ.get("INCIDENT_TEMPLATE") -def open_modal(client, body): - folders = list_folders() +def open_modal(client, body, folders): + if not folders: + return folder_names = [i["name"] for i in folders] blocks = [ {"type": "section", "text": {"type": "mrkdwn", "text": f"*{name}*"}} @@ -25,25 +27,10 @@ def open_modal(client, body): client.views_open(trigger_id=body["trigger_id"], view=view) -def google_service_command(client, body): - open_modal(client, body) - - -def list_folders(): - service = get_google_service("drive", "v3") - results = ( - service.files() - .list( - pageSize=25, - supportsAllDrives=True, - includeItemsFromAllDrives=True, - corpora="drive", - q="parents in '{}' and mimeType = 'application/vnd.google-apps.folder' and trashed=false and not name contains '{}'".format( - SRE_INCIDENT_FOLDER, "Templates" - ), - driveId=SRE_DRIVE_ID, - fields="nextPageToken, files(id, name)", - ) - .execute() - ) - return results.get("files", []) +def google_service_command(client, body, respond): + respond(f"Healthcheck status: {google_drive.healthcheck()}") + folders = google_drive.list_folders_in_folder(SRE_INCIDENT_FOLDER) + if not folders: + respond("The folder ID is invalid. Please check the environment variables.") + return + open_modal(client, body, folders) diff --git a/app/commands/sre.py b/app/commands/sre.py index 355f58bf..da3e43d5 100644 --- a/app/commands/sre.py +++ b/app/commands/sre.py @@ -21,6 +21,8 @@ \n - show the version of the SRE Bot \n - montre la version du bot SRE""" +PREFIX = os.environ.get("PREFIX", "") + def sre_command(ack, command, logger, respond, client, body): ack() @@ -47,8 +49,12 @@ def sre_command(ack, command, logger, respond, client, body): webhook_helper.handle_webhook_command(args, client, body, respond) case "version": respond(f"SRE Bot version: {os.environ.get('GIT_SHA', 'unknown')}") - case "google-service": - google_service.google_service_command(client, body) + case "google": + if PREFIX == "dev-": + google_service.google_service_command(client, body, respond) + else: + respond("This command is only available in the dev environment.") + return case _: respond( f"Unknown command: `{action}`. Type `/sre help` to see a list of commands. \nCommande inconnue: `{action}`. Entrez `/sre help` pour une liste des commandes valides" diff --git a/app/integrations/google_workspace/google_docs.py b/app/integrations/google_workspace/google_docs.py new file mode 100644 index 00000000..275ae601 --- /dev/null +++ b/app/integrations/google_workspace/google_docs.py @@ -0,0 +1,4 @@ +"""Google Docs module. + +This module provides functions to create and manipulate Google Docs. +""" diff --git a/app/integrations/google_workspace/google_drive.py b/app/integrations/google_workspace/google_drive.py new file mode 100644 index 00000000..ab4d0c05 --- /dev/null +++ b/app/integrations/google_workspace/google_drive.py @@ -0,0 +1,339 @@ +""" +Google Drive Module. + +This module provides functionalities to interact with Google Drive. It includes functions to add metadata to a file, create a new folder, create a new document from a template, and copy a file to a new folder. + +Functions: + add_metadata(file_id: str, key: str, value: str) -> dict: + Adds metadata to a file in Google Drive and returns the updated file metadata. + + delete_metadata(file_id: str, key: str) -> dict: + Deletes metadata from a file in Google Drive and returns the updated file metadata. + + list_metadata(file_id: str) -> dict: + Lists metadata of a file in Google Drive and returns the file metadata. + + create_folder(name: str, parent_folder: str) -> str: + Creates a new folder in Google Drive and returns the id of the new folder. + + create_file_from_template(name: str, folder: str, template: str) -> str: + Creates a new document in Google Drive from a template (Docs, Sheets, Slides, Forms, or Sites) and returns the id of the new document. + + create_file(name: str, folder: str, file_type: str) -> str: + Creates a new file in Google Drive and returns the id of the new file. + + get_file_by_name(name: str, folder: str) -> list: + Gets a file by name in Google Drive and returns a list of files that match the name. + + list_folders_in_folder(folder: str) -> list: + Lists all folders in a folder in Google Drive and returns a list of folders in the folder. + + copy_file_to_folder(file_id: str, name: str, parent_folder_id: str, destination_folder_id: str) -> str: + Copies a file to a new folder in Google Drive and returns the id of the new file. + + healthcheck() -> bool: + Checks the health of the Google Drive API and returns True if the API is healthy, False otherwise. +""" +import os +from integrations.google_workspace.google_service import ( + get_google_service, + handle_google_api_errors, +) + +INCIDENT_TEMPLATE = os.environ.get("INCIDENT_TEMPLATE") + + +@handle_google_api_errors +def add_metadata(file_id, key, value): + """Add metadata to a file in Google Drive. + + Args: + file_id (str): The file id of the file to add metadata to. + key (str): The key of the metadata to add. + value (str): The value of the metadata to add. + + Returns: + dict: The updated file metadata. + """ + # pylint: disable=no-member + service = get_google_service("drive", "v3") + result = ( + service.files() + .update( + fileId=file_id, + body={"appProperties": {key: value}}, + fields="name, appProperties", + supportsAllDrives=True, + ) + .execute() + ) + # pylint: enable=no-member + + return result + + +@handle_google_api_errors +def delete_metadata(file_id, key): + """Delete metadata from a file in Google Drive. + + Args: + file_id (str): The file id of the file to delete metadata from. + key (str): The key of the metadata to delete. + + Returns: + dict: The updated file metadata. + """ + service = get_google_service("drive", "v3") + result = ( + service.files() + .update( + fileId=file_id, + body={"appProperties": {key: None}}, + fields="name, appProperties", + supportsAllDrives=True, + ) + .execute() + ) + return result + + +@handle_google_api_errors +def list_metadata(file_id): + """List metadata of a file in Google Drive. + + Args: + file_id (str): The file id of the file to list metadata from. + + Returns: + dict: The file metadata. + """ + service = get_google_service("drive", "v3") + result = ( + service.files() + .get(fileId=file_id, fields="id, name, appProperties", supportsAllDrives=True) + .execute() + ) + return result + + +@handle_google_api_errors +def create_folder(name, parent_folder): + """Create a new folder in Google Drive. + + Args: + name (str): The name of the new folder. + parent_folder (str): The id of the parent folder. + + Returns: + str: The id of the new folder. + """ + # pylint: disable=no-member + service = get_google_service("drive", "v3") + results = ( + service.files() + .create( + body={ + "name": name, + "mimeType": "application/vnd.google-apps.folder", + "parents": [parent_folder], + }, + supportsAllDrives=True, + fields="id", + ) + .execute() + ) + # pylint: enable=no-member + + return results["id"] + + +@handle_google_api_errors +def create_file_from_template(name, folder, template): + """Create a new file in Google Drive from a template + (Docs, Sheets, Slides, Forms, or Sites.) + + Args: + name (str): The name of the new file. + folder (str): The id of the folder to create the file in. + template (str): The id of the template to use. + + Returns: + str: The id of the new file. + """ + # pylint: disable=no-member + service = get_google_service("drive", "v3") + result = ( + service.files() + .copy( + fileId=template, + body={"name": name, "parents": [folder]}, + supportsAllDrives=True, + ) + .execute() + ) + # pylint: enable=no-member + return result["id"] + + +@handle_google_api_errors +def create_file(name, folder, file_type): + """Create a new file in Google Drive. + Options for 'file_type' are: + + - "document": Google Docs + - "spreadsheet": Google Sheets + - "presentation": Google Slides + - "form": Google Forms + - "site": Google Sites + + Args: + name (str): The name of the new file. + folder (str): The id of the folder to create the file in. + file_type (str): The type of the new file. + + Returns: + str: The id of the new file. + """ + + mime_type = { + "document": "application/vnd.google-apps.document", + "spreadsheet": "application/vnd.google-apps.spreadsheet", + "presentation": "application/vnd.google-apps.presentation", + "form": "application/vnd.google-apps.form", + "site": "application/vnd.google-apps.site", + } + + if file_type not in mime_type: + raise ValueError(f"Invalid file_type: {file_type}") + + mime_type_value = mime_type[file_type] + + service = get_google_service("drive", "v3") + result = ( + service.files() + .create( + body={"name": name, "parents": [folder], "mimeType": mime_type_value}, + supportsAllDrives=True, + fields="id", + ) + .execute() + ) + return result["id"] + + +@handle_google_api_errors +def get_file_by_name(name, folder): + """Get a file by name in Google Drive. + + Args: + name (str): The name of the file to get. + folder (str): The id of the folder to search in. + + Returns: + list: A list of files that match the name. + """ + service = get_google_service("drive", "v3") + results = ( + service.files() + .list( + pageSize=1, + supportsAllDrives=True, + includeItemsFromAllDrives=True, + corpora="drive", + q="trashed=false and name='{}'".format(name), + driveId=folder, + fields="files(appProperties, id, name)", + ) + .execute() + ) + return results.get("files", []) + + +@handle_google_api_errors +def list_folders_in_folder(folder): + """List all folders in a folder in Google Drive. + + Args: + folder (str): The id of the folder to list. + + Returns: + list: A list of folders in the folder. + """ + service = get_google_service("drive", "v3") + page_token = None + all_files = [] + while True: + results = ( + service.files() + .list( + pageSize=25, + supportsAllDrives=True, + includeItemsFromAllDrives=True, + corpora="user", + q="parents in '{}' and mimeType = 'application/vnd.google-apps.folder' and trashed=false".format( + folder + ), + fields="nextPageToken, files(id, name)", + pageToken=page_token, + ) + .execute() + ) + all_files.extend(results.get("files", [])) + page_token = results.get("nextPageToken") + if not page_token: + break + return all_files + + +@handle_google_api_errors +def copy_file_to_folder(file_id, name, parent_folder_id, destination_folder_id): + """Copy a file to a new folder in Google Drive. + + Args: + file_id (str): The id of the file to copy. + name (str): The name of the new file. + parent_folder_id (str): The id of the parent folder. + destination_folder_id (str): The id of the destination folder. + + Returns: + str: The id of the new file. + """ + service = get_google_service("drive", "v3") + copied_file = ( + service.files() + .copy( + fileId=file_id, + body={"name": name, "parents": [parent_folder_id]}, + supportsAllDrives=True, + fields="id", + ) + .execute() + ) + # move the copy to the new folder + updated_file = ( + service.files() + .update( + fileId=copied_file["id"], + addParents=destination_folder_id, + removeParents=parent_folder_id, + supportsAllDrives=True, + fields="id", + ) + .execute() + ) + return updated_file["id"] + + +@handle_google_api_errors +def healthcheck(): + """Check the health of the Google Drive API. + + Returns: + bool: True if the API is healthy, False otherwise. + """ + healthy = False + metadata = list_metadata(INCIDENT_TEMPLATE) + if metadata is not None: + healthy = "id" in metadata + + return healthy diff --git a/app/integrations/google_workspace/google_meet.py b/app/integrations/google_workspace/google_meet.py index a481f76f..b8bf83db 100644 --- a/app/integrations/google_workspace/google_meet.py +++ b/app/integrations/google_workspace/google_meet.py @@ -1,4 +1,11 @@ -"""Google Meet integration.""" +"""Google Meet Module. + +This module provides a function to start a Google Meet session. + +Functions: + create_google_meet(title: str) -> str: + Starts a Google Meet session and returns the URL of the session. +""" import re from datetime import datetime diff --git a/app/integrations/google_workspace/google_service.py b/app/integrations/google_workspace/google_service.py index 587c7dad..6f375371 100644 --- a/app/integrations/google_workspace/google_service.py +++ b/app/integrations/google_workspace/google_service.py @@ -1,12 +1,26 @@ -"""Google Service Module.""" +""" +Google Service Module. + +This module provides a function to get an authenticated Google service and a decorator to handle Google API errors. + +Functions: + get_google_service(service: str, version: str) -> googleapiclient.discovery.Resource: + Returns an authenticated Google service resource for the specified service and version. + + handle_google_api_errors(func: Callable) -> Callable: + Decorator that catches and logs any HttpError or Error exceptions that occur when the decorated function is called. + +""" import os import logging import json from json import JSONDecodeError from dotenv import load_dotenv -from google.oauth2 import service_account -from googleapiclient.discovery import build +from functools import wraps +from google.oauth2 import service_account # type: ignore +from googleapiclient.discovery import build # type: ignore +from googleapiclient.errors import HttpError, Error # type: ignore load_dotenv() @@ -37,3 +51,30 @@ def get_google_service(service, version): msg="Invalid credentials JSON", doc="Credentials JSON", pos=0 ) from json_decode_exception return build(service, version, credentials=creds, cache_discovery=False) + + +def handle_google_api_errors(func): + """Decorator to handle Google API errors. + + Args: + func (function): The function to decorate. + + Returns: + The decorated function. + """ + + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except HttpError as e: + logging.error(f"An HTTP error occurred in function '{func.__name__}': {e}") + return None + except ValueError as e: + logging.error(f"A ValueError occurred in function '{func.__name__}': {e}") + return None + except Error as e: + logging.error(f"An error occurred in function '{func.__name__}': {e}") + return None + + return wrapper diff --git a/app/main.py b/app/main.py index 3d2c3c2d..6cbf48b6 100644 --- a/app/main.py +++ b/app/main.py @@ -26,10 +26,6 @@ def main(bot): APP_TOKEN = os.environ.get("APP_TOKEN") PREFIX = os.environ.get("PREFIX", "") - # Register Google Service command - bot.command(f"/{PREFIX}google-service")(google_service.google_service_command) - bot.view("google_service_view")(google_service.open_modal) - # Register Roles commands bot.command(f"/{PREFIX}talent-role")(role.role_command) bot.view("role_view")(role.role_view_handler) @@ -98,6 +94,11 @@ def main(bot): stop_run_continuously = scheduled_tasks.run_continuously() server_app.add_event_handler("shutdown", lambda: stop_run_continuously.set()) + # Register Google Service command for dev purposes only + if PREFIX == "dev-": + bot.command(f"/{PREFIX}google")(google_service.google_service_command) + bot.view("google_service_view")(google_service.open_modal) + def get_bot(): SLACK_TOKEN = os.environ.get("SLACK_TOKEN", None) diff --git a/app/mypy.ini b/app/mypy.ini new file mode 100644 index 00000000..0bb31247 --- /dev/null +++ b/app/mypy.ini @@ -0,0 +1,2 @@ +[mypy-google.oauth2.service_account] +ignore_missing_imports = True \ No newline at end of file diff --git a/app/tests/integrations/google_workspace/test_google_drive_module.py b/app/tests/integrations/google_workspace/test_google_drive_module.py new file mode 100644 index 00000000..5d689601 --- /dev/null +++ b/app/tests/integrations/google_workspace/test_google_drive_module.py @@ -0,0 +1,155 @@ +"""Unit tests for google_drive module.""" +from unittest.mock import patch + +import integrations.google_workspace.google_drive as google_drive + + +@patch("integrations.google_workspace.google_drive.get_google_service") +def test_add_metadata_returns_result(get_google_service_mock): + get_google_service_mock.return_value.files.return_value.update.return_value.execute.return_value = { + "name": "test_folder", + "appProperties": {"key": "value"}, + } + result = google_drive.add_metadata("file_id", "key", "value") + assert result == {"name": "test_folder", "appProperties": {"key": "value"}} + + +@patch("integrations.google_workspace.google_drive.get_google_service") +def test_delete_metadata_returns_result(get_google_service_mock): + get_google_service_mock.return_value.files.return_value.update.return_value.execute.return_value = { + "name": "test_folder", + "appProperties": {}, + } + result = google_drive.delete_metadata("file_id", "key") + get_google_service_mock.return_value.files.return_value.update.assert_called_once_with( + fileId="file_id", + body={"appProperties": {"key": None}}, + fields="name, appProperties", + supportsAllDrives=True, + ) + assert result == {"name": "test_folder", "appProperties": {}} + + +@patch("integrations.google_workspace.google_drive.get_google_service") +def test_list_metadata_returns_result(get_google_service_mock): + get_google_service_mock.return_value.files.return_value.get.return_value.execute.return_value = { + "name": "test_folder", + "appProperties": {"key": "value"}, + } + assert google_drive.list_metadata("file_id") == { + "name": "test_folder", + "appProperties": {"key": "value"}, + } + + +@patch("integrations.google_workspace.google_drive.get_google_service") +def test_create_folder_returns_folder_id(get_google_service_mock): + get_google_service_mock.return_value.files.return_value.create.return_value.execute.return_value = { + "id": "test_folder_id" + } + assert ( + google_drive.create_folder("test_folder", "parent_folder") == "test_folder_id" + ) + + +@patch("integrations.google_workspace.google_drive.get_google_service") +def test_create_file_with_valid_type_returns_file_id(get_google_service_mock): + get_google_service_mock.return_value.files.return_value.create.return_value.execute.return_value = { + "id": "test_document_id" + } + result = google_drive.create_file("test_document", "folder_id", "document") + assert result == "test_document_id" + + +@patch("logging.error") +@patch("integrations.google_workspace.google_drive.get_google_service") +def test_create_file_with_invalid_type_raises_value_error( + get_google_service_mock, mocked_logging_error +): + result = google_drive.create_file("name", "folder", "invalid_file_type") + + assert result is None + mocked_logging_error.assert_called_once_with( + "A ValueError occurred in function 'create_file': Invalid file_type: invalid_file_type" + ) + + +@patch("integrations.google_workspace.google_drive.get_google_service") +def test_create_file_from_template_returns_file_id(get_google_service_mock): + get_google_service_mock.return_value.files.return_value.copy.return_value.execute.return_value = { + "id": "test_document_id" + } + result = google_drive.create_file_from_template( + "test_document", "folder_id", "template_id" + ) + assert result == "test_document_id" + + +@patch("integrations.google_workspace.google_drive.get_google_service") +def test_get_file_by_name_returns_object(get_google_service_mock): + get_google_service_mock.return_value.files.return_value.list.return_value.execute.return_value = { + "files": [ + { + "name": "test_document", + "id": "test_document_id", + "appProperties": {}, + }, + ] + } + assert google_drive.get_file_by_name("test_file_name", "folder_id") == [ + { + "name": "test_document", + "id": "test_document_id", + "appProperties": {}, + } + ] + + +@patch("integrations.google_workspace.google_drive.get_google_service") +def test_copy_file_to_folder_returns_file_id(get_google_service_mock): + get_google_service_mock.return_value.files.return_value.copy.return_value.execute.return_value = { + "id": "file_id" + } + get_google_service_mock.return_value.files.return_value.update.return_value.execute.return_value = { + "id": "updated_file_id" + } + assert ( + google_drive.copy_file_to_folder( + "file_id", "name", "parent_folder", "destination_folder" + ) + == "updated_file_id" + ) + + +@patch("integrations.google_workspace.google_drive.get_google_service") +def test_list_folders_returns_folder_names(get_google_service_mock): + get_google_service_mock.return_value.files.return_value.list.return_value.execute.return_value = { + "files": [{"name": "test_folder"}] + } + assert google_drive.list_folders_in_folder("parent_folder") == [ + {"name": "test_folder"} + ] + + +@patch("integrations.google_workspace.google_drive.get_google_service") +def test_list_folders_iterates_over_pages(get_google_service_mock): + get_google_service_mock.return_value.files.return_value.list.return_value.execute.side_effect = [ + {"files": [{"name": "test_folder"}], "nextPageToken": "token"}, + {"files": [{"name": "test_folder2"}]}, + ] + assert google_drive.list_folders_in_folder("parent_folder") == [ + {"name": "test_folder"}, + {"name": "test_folder2"}, + ] + + +@patch("integrations.google_workspace.google_drive.list_metadata") +def test_healthcheck_healthy(list_metadata_mock): + list_metadata_mock.return_value = {"id": "test_doc"} + assert google_drive.healthcheck() is True + + +@patch("integrations.google_workspace.google_drive.list_metadata") +def test_healthcheck_unhealthy(list_metadata_mock): + list_metadata_mock.return_value = None + assert google_drive.healthcheck() is False diff --git a/app/tests/integrations/google_workspace/test_google_service.py b/app/tests/integrations/google_workspace/test_google_service.py index 4c24b91a..efee4cc3 100644 --- a/app/tests/integrations/google_workspace/test_google_service.py +++ b/app/tests/integrations/google_workspace/test_google_service.py @@ -1,14 +1,18 @@ +"""Unit Tests for the google_service module.""" import json -import pytest from unittest.mock import patch, MagicMock -from integrations.google_workspace.google_service import get_google_service from json import JSONDecodeError +import pytest +from google.oauth2.service_account import Credentials +from integrations.google_workspace.google_service import ( + get_google_service, + handle_google_api_errors, +) +from googleapiclient.errors import HttpError, Error # type: ignore @patch("integrations.google_workspace.google_service.build") -@patch( - "integrations.google_workspace.google_service.service_account.Credentials.from_service_account_info" -) +@patch.object(Credentials, "from_service_account_info") def test_get_google_service_returns_build_object(credentials_mock, build_mock): """ Test case to verify that the function returns a build object. @@ -36,9 +40,7 @@ def test_get_google_service_raises_exception_if_credentials_json_not_set(): @patch("integrations.google_workspace.google_service.build") -@patch( - "integrations.google_workspace.google_service.service_account.Credentials.from_service_account_info" -) +@patch.object(Credentials, "from_service_account_info") def test_get_google_service_raises_exception_if_credentials_json_is_invalid( credentials_mock, build_mock ): @@ -50,3 +52,60 @@ def test_get_google_service_raises_exception_if_credentials_json_is_invalid( with pytest.raises(JSONDecodeError) as e: get_google_service("drive", "v3") assert "Invalid credentials JSON" in str(e.value) + + +@patch("logging.error") +def test_handle_google_api_errors_catches_http_error(mocked_logging_error): + mock_resp = MagicMock() + mock_resp.status = "400" + mock_resp.reason = "Bad Request" + mock_func = MagicMock(side_effect=HttpError(resp=mock_resp, content=b"")) + mock_func.__name__ = "mock_func" + decorated_func = handle_google_api_errors(mock_func) + + result = decorated_func() + + assert result is None + mocked_logging_error.assert_called_once_with( + "An HTTP error occurred in function 'mock_func': " + ) + + +@patch("logging.error") +def test_handle_google_api_errors_catches_value_error(mocked_logging_error): + mock_func = MagicMock(side_effect=ValueError("ValueError message")) + mock_func.__name__ = "mock_func" + decorated_func = handle_google_api_errors(mock_func) + + result = decorated_func() + + assert result is None + mock_func.assert_called_once() + mocked_logging_error.assert_called_once_with( + "A ValueError occurred in function 'mock_func': ValueError message" + ) + + +@patch("logging.error") +def test_handle_google_api_errors_catches_error(mocked_logging_error): + mock_func = MagicMock(side_effect=Error("Error message")) + mock_func.__name__ = "mock_func" + decorated_func = handle_google_api_errors(mock_func) + + result = decorated_func() + + assert result is None + mock_func.assert_called_once() + mocked_logging_error.assert_called_once_with( + "An error occurred in function 'mock_func': Error message" + ) + + +def test_handle_google_api_errors_passes_through_return_value(): + mock_func = MagicMock(return_value="test") + decorated_func = handle_google_api_errors(mock_func) + + result = decorated_func() + + assert result == "test" + mock_func.assert_called_once()