diff --git a/app/integrations/google_workspace/google_directory.py b/app/integrations/google_workspace/google_directory.py index 32abe291..9f07a39a 100644 --- a/app/integrations/google_workspace/google_directory.py +++ b/app/integrations/google_workspace/google_directory.py @@ -1,21 +1,20 @@ -"""Google Directory module for interacting with the Google Workspace Directory API.""" +"""Google Directory module to interact with the Google Workspace Directory API.""" -import os from integrations.google_workspace.google_service import ( - get_google_service, handle_google_api_errors, + execute_google_api_call, + DEFAULT_DELEGATED_ADMIN_EMAIL, + DEFAULT_GOOGLE_WORKSPACE_CUSTOMER_ID, ) -GOOGLE_DELEGATED_ADMIN_EMAIL = os.environ.get("GOOGLE_DELEGATED_ADMIN_EMAIL") -GOOGLE_WORKSPACE_CUSTOMER_ID = os.environ.get("GOOGLE_WORKSPACE_CUSTOMER_ID") - @handle_google_api_errors -def get_user(user_key): +def get_user(user_key, delegated_user_email=None): """Get a user by user key in the Google Workspace domain. Args: user_key (str): The user's primary email address, alias email address, or unique user ID. + delegated_user_email (str): The email address of the user to impersonate. (default: must be defined in .env) Returns: dict: A user object. @@ -23,57 +22,54 @@ def get_user(user_key): Ref: https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/get """ + if not delegated_user_email: + delegated_user_email = DEFAULT_DELEGATED_ADMIN_EMAIL scopes = ["https://www.googleapis.com/auth/admin.directory.user.readonly"] - - service = get_google_service( + return execute_google_api_call( "admin", "directory_v1", - delegated_user_email=GOOGLE_DELEGATED_ADMIN_EMAIL, - scopes=scopes, + "users", + "get", + scopes, + delegated_user_email, + userKey=user_key, ) - user = service.users().get(userKey=user_key).execute() - return user @handle_google_api_errors -def list_users(): +def list_users( + delegated_user_email=None, + customer=None, +): """List all users in the Google Workspace domain. Returns: list: A list of user objects. - - Ref: https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list """ - + if not delegated_user_email: + delegated_user_email = DEFAULT_DELEGATED_ADMIN_EMAIL + if not customer: + customer = DEFAULT_GOOGLE_WORKSPACE_CUSTOMER_ID scopes = ["https://www.googleapis.com/auth/admin.directory.user.readonly"] - service = get_google_service( + return execute_google_api_call( "admin", "directory_v1", - delegated_user_email=GOOGLE_DELEGATED_ADMIN_EMAIL, - scopes=scopes, + "users", + "list", + scopes, + delegated_user_email, + paginate=True, + customer=customer, + maxResults=10, + orderBy="email", ) - page_token = None - all_users = [] - while True: - results = ( - service.users() - .list( - customer=GOOGLE_WORKSPACE_CUSTOMER_ID, - maxResults=500, - orderBy="email", - pageToken=page_token, - ) - .execute() - ) - all_users.extend(results.get("users", [])) - page_token = results.get("nextPageToken") - if not page_token: - break - return all_users @handle_google_api_errors -def list_groups(): +def list_groups( + delegated_user_email=None, + customer=None, +): """List all groups in the Google Workspace domain. Returns: @@ -81,39 +77,27 @@ def list_groups(): Ref: https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/list """ - + if not delegated_user_email: + delegated_user_email = DEFAULT_DELEGATED_ADMIN_EMAIL + if not customer: + customer = DEFAULT_GOOGLE_WORKSPACE_CUSTOMER_ID scopes = ["https://www.googleapis.com/auth/admin.directory.group.readonly"] - - service = get_google_service( + return execute_google_api_call( "admin", "directory_v1", - delegated_user_email=GOOGLE_DELEGATED_ADMIN_EMAIL, - scopes=scopes, + "groups", + "list", + scopes, + delegated_user_email, + paginate=True, + customer=customer, + maxResults=100, + orderBy="email", ) - all_groups = [] - page_token = None - - while True: - results = ( - service.groups() - .list( - customer=GOOGLE_WORKSPACE_CUSTOMER_ID, - maxResults=500, - orderBy="email", - pageToken=page_token, - ) - .execute() - ) - all_groups.extend(results.get("groups", [])) - page_token = results.get("nextPageToken") - if not page_token: - break - - return all_groups @handle_google_api_errors -def list_group_members(group_key): +def list_group_members(group_key, delegated_user_email=None): """List all group members in the Google Workspace domain. Returns: @@ -122,30 +106,16 @@ def list_group_members(group_key): Ref: https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/list """ + if not delegated_user_email: + delegated_user_email = DEFAULT_DELEGATED_ADMIN_EMAIL scopes = ["https://www.googleapis.com/auth/admin.directory.group.member.readonly"] - - service = get_google_service( + return execute_google_api_call( "admin", "directory_v1", - delegated_user_email=GOOGLE_DELEGATED_ADMIN_EMAIL, - scopes=scopes, + "members", + "list", + scopes, + delegated_user_email, + paginate=True, + groupKey=group_key, ) - all_group_members = [] - page_token = None - - while True: - results = ( - service.members() - .list( - groupKey=group_key, - maxResults=500, - pageToken=page_token, - ) - .execute() - ) - all_group_members.extend(results.get("members", [])) - page_token = results.get("nextPageToken") - if not page_token: - break - - return all_group_members diff --git a/app/integrations/google_workspace/google_docs.py b/app/integrations/google_workspace/google_docs.py index 6874aebd..04259d96 100644 --- a/app/integrations/google_workspace/google_docs.py +++ b/app/integrations/google_workspace/google_docs.py @@ -1,22 +1,12 @@ """Google Docs module. This module provides functions to create and manipulate Google Docs. - -Functions: - create(title: str) -> str: - Creates a new document in Google Docs and returns the id of the new document. - - batch_update(document_id: str, requests: list) -> None: - Applies a list of updates to a document in Google Docs. - - get(document_id: str) -> dict: - Gets a document from Google Docs and returns the document resource. """ import logging import re from integrations.google_workspace.google_service import ( - get_google_service, handle_google_api_errors, + execute_google_api_call, ) @@ -30,9 +20,14 @@ def create(title: str) -> str: Returns: str: The id of the new document. """ - # pylint: disable=no-member - service = get_google_service("docs", "v1") - result = service.documents().create(body={"title": title}).execute() + result = execute_google_api_call( + "docs", + "v1", + "documents", + "create", + scopes=["https://www.googleapis.com/auth/documents"], + body={"title": title}, + ) return result["documentId"] @@ -47,12 +42,15 @@ def batch_update(document_id: str, requests: list) -> None: Returns: None """ - # pylint: disable=no-member - service = get_google_service("docs", "v1") - service.documents().batchUpdate( + execute_google_api_call( + "docs", + "v1", + "documents", + "batchUpdate", + scopes=["https://www.googleapis.com/auth/documents"], documentId=document_id, body={"requests": requests}, - ).execute() + ) @handle_google_api_errors @@ -65,10 +63,14 @@ def get(document_id: str) -> dict: Returns: dict: The document resource. """ - # pylint: disable=no-member - service = get_google_service("docs", "v1") - result = service.documents().get(documentId=document_id).execute() - return result + return execute_google_api_call( + "docs", + "v1", + "documents", + "get", + scopes=["https://www.googleapis.com/auth/documents.readonly"], + documentId=document_id, + ) def extract_google_doc_id(url): diff --git a/app/integrations/google_workspace/google_drive.py b/app/integrations/google_workspace/google_drive.py index ab4d0c05..6e6dbd81 100644 --- a/app/integrations/google_workspace/google_drive.py +++ b/app/integrations/google_workspace/google_drive.py @@ -2,42 +2,12 @@ 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, + execute_google_api_call, ) INCIDENT_TEMPLATE = os.environ.get("INCIDENT_TEMPLATE") @@ -55,21 +25,16 @@ def add_metadata(file_id, key, value): 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() + return execute_google_api_call( + "drive", + "v3", + "files", + "update", + fileId=file_id, + body={"appProperties": {key: value}}, + fields="name, appProperties", + supportsAllDrives=True, ) - # pylint: enable=no-member - - return result @handle_google_api_errors @@ -83,18 +48,16 @@ def delete_metadata(file_id, key): 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 execute_google_api_call( + "drive", + "v3", + "files", + "update", + fileId=file_id, + body={"appProperties": {key: None}}, + fields="name, appProperties", + supportsAllDrives=True, ) - return result @handle_google_api_errors @@ -107,13 +70,15 @@ def list_metadata(file_id): 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 execute_google_api_call( + "drive", + "v3", + "files", + "get", + fileId=file_id, + fields="id, name, appProperties", + supportsAllDrives=True, ) - return result @handle_google_api_errors @@ -127,24 +92,21 @@ def create_folder(name, 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() + result = execute_google_api_call( + "drive", + "v3", + "files", + "create", + body={ + "name": name, + "mimeType": "application/vnd.google-apps.folder", + "parents": [parent_folder], + }, + supportsAllDrives=True, + fields="id", ) - # pylint: enable=no-member - return results["id"] + return result["id"] @handle_google_api_errors @@ -160,18 +122,17 @@ def create_file_from_template(name, folder, template): 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() + result = execute_google_api_call( + "drive", + "v3", + "files", + "copy", + fileId=template, + body={"name": name, "parents": [folder]}, + supportsAllDrives=True, + fields="id", ) - # pylint: enable=no-member + return result["id"] @@ -208,45 +169,49 @@ def create_file(name, folder, 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() + result = execute_google_api_call( + "drive", + "v3", + "files", + "create", + body={"name": name, "parents": [folder], "mimeType": mime_type_value}, + supportsAllDrives=True, + fields="id", ) + return result["id"] @handle_google_api_errors -def get_file_by_name(name, folder): - """Get a file by name in Google Drive. +def get_file_by_name(name, folder_id=None): + """Get a file by name in a specific Google Drive folder. + + This function requires the caller to have the necessary permissions to access the file in Google Workspace. Args: name (str): The name of the file to get. - folder (str): The id of the folder to search in. + folder_id (str, optional): The id of the folder to search in. If None, search in all folders. Returns: - list: A list of files that match the name. + list: A list of files that match the name within the folder. """ - 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() + q = f"trashed=false and name='{name}'" + if folder_id: + q += f" and '{folder_id}' in parents" + return execute_google_api_call( + "drive", + "v3", + "files", + "list", + scopes=["https://www.googleapis.com/auth/drive.readonly"], + paginate=True, + pageSize=1, + supportsAllDrives=True, + includeItemsFromAllDrives=True, + corpora="user", + q=q, + fields="files(appProperties, id, name)", ) - return results.get("files", []) @handle_google_api_errors @@ -259,30 +224,44 @@ def list_folders_in_folder(folder): 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 + return execute_google_api_call( + "drive", + "v3", + "files", + "list", + paginate=True, + pageSize=25, + supportsAllDrives=True, + includeItemsFromAllDrives=True, + corpora="user", + q=f"parents in '{folder}' and mimeType = 'application/vnd.google-apps.folder' and trashed=false", + fields="files(id, name)", + ) + + +@handle_google_api_errors +def list_files_in_folder(folder): + """List all files in a folder in Google Drive. + + Args: + folder (str): The id of the folder to list. + + Returns: + list: A list of files in the folder. + """ + return execute_google_api_call( + "drive", + "v3", + "files", + "list", + paginate=True, + pageSize=25, + supportsAllDrives=True, + includeItemsFromAllDrives=True, + corpora="user", + q=f"parents in '{folder}' and mimeType != 'application/vnd.google-apps.folder' and trashed=false", + fields="files(id, name)", + ) @handle_google_api_errors @@ -298,29 +277,30 @@ def copy_file_to_folder(file_id, name, parent_folder_id, destination_folder_id): 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() + copied_file = execute_google_api_call( + "drive", + "v3", + "files", + "copy", + fileId=file_id, + body={"name": name, "parents": [parent_folder_id]}, + supportsAllDrives=True, + fields="id", ) + # 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() + updated_file = execute_google_api_call( + "drive", + "v3", + "files", + "update", + fileId=copied_file["id"], + addParents=destination_folder_id, + removeParents=parent_folder_id, + supportsAllDrives=True, + fields="id", ) + return updated_file["id"] diff --git a/app/tests/integrations/google_workspace/test_google_directory.py b/app/tests/integrations/google_workspace/test_google_directory.py index da73d6ff..0e45224c 100644 --- a/app/tests/integrations/google_workspace/test_google_directory.py +++ b/app/tests/integrations/google_workspace/test_google_directory.py @@ -1,12 +1,15 @@ """Unit tests for google_directory module.""" from unittest.mock import patch +from integrations.google_workspace import google_directory -import integrations.google_workspace.google_directory as google_directory - -@patch("integrations.google_workspace.google_directory.get_google_service") -def test_get_user_returns_user(get_google_service_mock): - get_google_service_mock.return_value.users.return_value.get.return_value.execute.return_value = { +@patch( + "integrations.google_workspace.google_directory.DEFAULT_DELEGATED_ADMIN_EMAIL", + new="default_delegated_admin_email", +) +@patch("integrations.google_workspace.google_directory.execute_google_api_call") +def test_get_user_returns_user(execute_google_api_call_mock): + execute_google_api_call_mock.return_value = { "id": "test_user_id", "name": "test_user", "email": "user.name@domain.com", @@ -18,117 +21,223 @@ def test_get_user_returns_user(get_google_service_mock): "email": "user.name@domain.com", } + execute_google_api_call_mock.assert_called_once_with( + "admin", + "directory_v1", + "users", + "get", + ["https://www.googleapis.com/auth/admin.directory.user.readonly"], + "default_delegated_admin_email", + userKey="test_user_id", + ) + + +@patch("integrations.google_workspace.google_directory.execute_google_api_call") +def test_get_user_uses_custom_delegated_user_email_if_provided( + execute_google_api_call_mock, +): + execute_google_api_call_mock.return_value = { + "id": "test_user_id", + "name": "test_user", + "email": "user.name@domain.com", + } -@patch("integrations.google_workspace.google_directory.get_google_service") -def test_list_users_returns_users(get_google_service_mock): - get_google_service_mock.return_value.users.return_value.list.return_value.execute.return_value = { - "users": [ - {"id": "test_user_id", "name": "test_user", "email": "email@domain.com"}, - {"id": "test_user_id2", "name": "test_user2", "email": "email2@domain.com"}, - ] + custom_delegated_user_email = "custom.email@domain.com" + assert google_directory.get_user("test_user_id", custom_delegated_user_email) == { + "id": "test_user_id", + "name": "test_user", + "email": "user.name@domain.com", } - assert google_directory.list_users() == [ + execute_google_api_call_mock.assert_called_once_with( + "admin", + "directory_v1", + "users", + "get", + ["https://www.googleapis.com/auth/admin.directory.user.readonly"], + "custom.email@domain.com", + userKey="test_user_id", + ) + + +@patch( + "integrations.google_workspace.google_directory.DEFAULT_GOOGLE_WORKSPACE_CUSTOMER_ID", + new="default_google_workspace_customer_id", +) +@patch( + "integrations.google_workspace.google_directory.DEFAULT_DELEGATED_ADMIN_EMAIL", + new="default_delegated_admin_email", +) +@patch("integrations.google_workspace.google_directory.execute_google_api_call") +def test_list_users_returns_users(execute_google_api_call_mock): + # Mock the results + results = [ {"id": "test_user_id", "name": "test_user", "email": "email@domain.com"}, {"id": "test_user_id2", "name": "test_user2", "email": "email2@domain.com"}, ] - -@patch("integrations.google_workspace.google_directory.get_google_service") -def test_list_users_iterates_over_pages(get_google_service_mock): - get_google_service_mock.return_value.users.return_value.list.return_value.execute.side_effect = [ - { - "users": [ - {"id": "test_user_id", "name": "test_user", "email": "email@domain.com"} - ], - "nextPageToken": "token", - }, - { - "users": [ - { - "id": "test_user_id2", - "name": "test_user2", - "email": "email2@domain.com", - } - ] - }, - ] - - assert google_directory.list_users() == [ + execute_google_api_call_mock.return_value = results + + assert google_directory.list_users() == results + + execute_google_api_call_mock.assert_called_once_with( + "admin", + "directory_v1", + "users", + "list", + ["https://www.googleapis.com/auth/admin.directory.user.readonly"], + "default_delegated_admin_email", + paginate=True, + customer="default_google_workspace_customer_id", + maxResults=10, + orderBy="email", + ) + + +@patch("integrations.google_workspace.google_directory.execute_google_api_call") +def test_list_users_uses_custom_delegated_user_email_and_customer_id_if_provided( + execute_google_api_call_mock, +): + # Mock the results + results = [ {"id": "test_user_id", "name": "test_user", "email": "email@domain.com"}, {"id": "test_user_id2", "name": "test_user2", "email": "email2@domain.com"}, ] - -@patch("integrations.google_workspace.google_directory.get_google_service") -def test_list_groups_returns_groups(get_google_service_mock): - get_google_service_mock.return_value.groups.return_value.list.return_value.execute.return_value = { - "groups": [ - {"id": "test_group_id", "name": "test_group"}, - {"id": "test_group_id2", "name": "test_group2"}, - ] - } - - assert google_directory.list_groups() == [ - {"id": "test_group_id", "name": "test_group"}, - {"id": "test_group_id2", "name": "test_group2"}, - ] - - -@patch("integrations.google_workspace.google_directory.get_google_service") -def test_list_groups_iterates_over_pages(get_google_service_mock): - get_google_service_mock.return_value.groups.return_value.list.return_value.execute.side_effect = [ - { - "groups": [{"id": "test_group_id", "name": "test_group"}], - "nextPageToken": "token", - }, - { - "groups": [{"id": "test_group_id2", "name": "test_group2"}], - }, + execute_google_api_call_mock.return_value = results + + custom_delegated_user_email = "custom.email@domain.com" + custom_customer_id = "custom_customer_id" + + assert ( + google_directory.list_users(custom_delegated_user_email, custom_customer_id) + == results + ) + + execute_google_api_call_mock.assert_called_once_with( + "admin", + "directory_v1", + "users", + "list", + ["https://www.googleapis.com/auth/admin.directory.user.readonly"], + custom_delegated_user_email, + paginate=True, + customer=custom_customer_id, + maxResults=10, + orderBy="email", + ) + + +@patch( + "integrations.google_workspace.google_directory.DEFAULT_GOOGLE_WORKSPACE_CUSTOMER_ID", + new="default_google_workspace_customer_id", +) +@patch( + "integrations.google_workspace.google_directory.DEFAULT_DELEGATED_ADMIN_EMAIL", + new="default_delegated_admin_email", +) +@patch("integrations.google_workspace.google_directory.execute_google_api_call") +def test_list_groups_calls_execute_google_api_call_with_correct_args( + mock_execute_google_api_call, +): + google_directory.list_groups() + mock_execute_google_api_call.assert_called_once_with( + "admin", + "directory_v1", + "groups", + "list", + ["https://www.googleapis.com/auth/admin.directory.group.readonly"], + "default_delegated_admin_email", + paginate=True, + customer="default_google_workspace_customer_id", + maxResults=100, + orderBy="email", + ) + + +@patch("integrations.google_workspace.google_directory.execute_google_api_call") +def test_list_groups_uses_custom_delegated_user_email_and_customer_id_if_provided( + execute_google_api_call_mock, +): + # Mock the results + results = [ + {"id": "test_group_id", "name": "test_group", "email": "email@domain.com"}, + {"id": "test_group_id2", "name": "test_group2", "email": "email2@domain.com"}, ] - assert google_directory.list_groups() == [ - {"id": "test_group_id", "name": "test_group"}, - {"id": "test_group_id2", "name": "test_group2"}, + execute_google_api_call_mock.return_value = results + + custom_delegated_user_email = "custom.email@domain.com" + custom_customer_id = "custom_customer_id" + + assert ( + google_directory.list_groups(custom_delegated_user_email, custom_customer_id) + == results + ) + + execute_google_api_call_mock.assert_called_once_with( + "admin", + "directory_v1", + "groups", + "list", + ["https://www.googleapis.com/auth/admin.directory.group.readonly"], + custom_delegated_user_email, + paginate=True, + customer=custom_customer_id, + maxResults=100, + orderBy="email", + ) + + +@patch( + "integrations.google_workspace.google_directory.DEFAULT_DELEGATED_ADMIN_EMAIL", + new="default_delegated_admin_email", +) +@patch("integrations.google_workspace.google_directory.execute_google_api_call") +def test_list_group_members_calls_execute_google_api_call_with_correct_args( + mock_execute_google_api_call, +): + group_key = "test_group_key" + google_directory.list_group_members(group_key) + mock_execute_google_api_call.assert_called_once_with( + "admin", + "directory_v1", + "members", + "list", + ["https://www.googleapis.com/auth/admin.directory.group.member.readonly"], + "default_delegated_admin_email", + paginate=True, + groupKey=group_key, + ) + + +@patch("integrations.google_workspace.google_directory.execute_google_api_call") +def test_list_group_members_uses_custom_delegated_user_email_if_provided( + execute_google_api_call_mock, +): + # Mock the results + results = [ + {"id": "test_member_id", "email": "member@domain.com"}, + {"id": "test_member_id2", "email": "member2@domain.com"}, ] - -@patch("integrations.google_workspace.google_directory.get_google_service") -def test_list_group_members_returns_group_members(get_google_service_mock): - get_google_service_mock.return_value.members.return_value.list.return_value.execute.return_value = { - "members": [ - {"id": "test_user_id", "name": "test_user", "email": "email@domain.com"}, - {"id": "test_user_id2", "name": "test_user2", "email": "email2@domain.com"}, - ] - } - - assert google_directory.list_group_members("test_group_id") == [ - {"id": "test_user_id", "name": "test_user", "email": "email@domain.com"}, - {"id": "test_user_id2", "name": "test_user2", "email": "email2@domain.com"}, - ] - - -@patch("integrations.google_workspace.google_directory.get_google_service") -def test_list_group_members_iterates_over_pages(get_google_service_mock): - get_google_service_mock.return_value.members.return_value.list.return_value.execute.side_effect = [ - { - "members": [ - {"id": "test_user_id", "name": "test_user", "email": "email@domain.com"} - ], - "nextPageToken": "token", - }, - { - "members": [ - { - "id": "test_user_id2", - "name": "test_user2", - "email": "email2@domain.com", - } - ], - }, - ] - - assert google_directory.list_group_members("test_group_id") == [ - {"id": "test_user_id", "name": "test_user", "email": "email@domain.com"}, - {"id": "test_user_id2", "name": "test_user2", "email": "email2@domain.com"}, - ] + execute_google_api_call_mock.return_value = results + + group_key = "test_group_key" + custom_delegated_user_email = "custom.email@domain.com" + + assert ( + google_directory.list_group_members(group_key, custom_delegated_user_email) + == results + ) + + execute_google_api_call_mock.assert_called_once_with( + "admin", + "directory_v1", + "members", + "list", + ["https://www.googleapis.com/auth/admin.directory.group.member.readonly"], + custom_delegated_user_email, + paginate=True, + groupKey=group_key, + ) diff --git a/app/tests/integrations/google_workspace/test_google_docs.py b/app/tests/integrations/google_workspace/test_google_docs.py index d0cfd461..45239fe5 100644 --- a/app/tests/integrations/google_workspace/test_google_docs.py +++ b/app/tests/integrations/google_workspace/test_google_docs.py @@ -1,42 +1,60 @@ -"""Unit tests for google_docs module.""" from unittest.mock import patch +from integrations.google_workspace import google_docs -import integrations.google_workspace.google_docs as google_docs - -@patch("integrations.google_workspace.google_docs.get_google_service") -def test_create_returns_document_id(get_google_service_mock): - # the api states: Creates a blank document using the title given in the request. Other fields in the request, including any provided content, are ignored. - get_google_service_mock.return_value.documents.return_value.create.return_value.execute.return_value = { +@patch("integrations.google_workspace.google_docs.execute_google_api_call") +def test_create_returns_document_id(execute_google_api_call_mock): + # Mock the return value of execute_google_api_call + execute_google_api_call_mock.return_value = { "documentId": "test_document_id", "title": "test_document", "body": {"content": [{}]}, "headers": {}, } - assert google_docs.create("test_document") == "test_document_id" + # Call the create function + document_id = google_docs.create("test_document") -@patch("integrations.google_workspace.google_docs.get_google_service") -def test_batch_update_with_valid_requests_succeeds(get_google_service_mock): - get_google_service_mock.return_value.documents.return_value.batchUpdate.return_value.execute.return_value = { - "responses": [{"headerId": "test_header_id"}, {}, {}] - } + # Check that the create function returns the correct document id + assert document_id == "test_document_id" + + # Check that execute_google_api_call was called with the correct arguments + execute_google_api_call_mock.assert_called_once_with( + "docs", + "v1", + "documents", + "create", + scopes=["https://www.googleapis.com/auth/documents"], + body={"title": "test_document"}, + ) + +@patch("integrations.google_workspace.google_docs.execute_google_api_call") +def test_batch_update_with_valid_requests_succeeds(execute_google_api_call_mock): requests = [ {"createHeader": {"type": "DEFAULT", "sectionBreakLocation": {"index": 1}}}, {"insertText": {"location": {"index": 2}, "text": "Hello world"}}, {"insertText": {"location": {"index": 3}, "text": "Foo"}}, ] - assert google_docs.batch_update("test_document_id", requests) is None - get_google_service_mock.return_value.documents.return_value.batchUpdate.assert_called_once_with( - documentId="test_document_id", body={"requests": requests} + google_docs.batch_update("test_document_id", requests) + + # Check that execute_google_api_call was called with the correct arguments + execute_google_api_call_mock.assert_called_once_with( + "docs", + "v1", + "documents", + "batchUpdate", + scopes=["https://www.googleapis.com/auth/documents"], + documentId="test_document_id", + body={"requests": requests}, ) -@patch("integrations.google_workspace.google_docs.get_google_service") -def test_get_returns_document_resource(get_google_service_mock): - get_google_service_mock.return_value.documents.return_value.get.return_value.execute.return_value = { +@patch("integrations.google_workspace.google_docs.execute_google_api_call") +def test_get_returns_document_resource(execute_google_api_call_mock): + # Mock the return value of execute_google_api_call + execute_google_api_call_mock.return_value = { "documentId": "test_document_id", "title": "test_document", "body": {"content": [{}]}, @@ -49,9 +67,19 @@ def test_get_returns_document_resource(get_google_service_mock): } document = google_docs.get("test_document_id") - assert document["documentId"] == "test_document_id" - assert document["title"] == "test_document" - assert document["body"] == {"content": [{}]} + + # Check that the get function returns the correct document + assert document == execute_google_api_call_mock.return_value + + # Check that execute_google_api_call was called with the correct arguments + execute_google_api_call_mock.assert_called_once_with( + "docs", + "v1", + "documents", + "get", + scopes=["https://www.googleapis.com/auth/documents.readonly"], + documentId="test_document_id", + ) def test_extract_googe_doc_id_valid_google_docs_url(): diff --git a/app/tests/integrations/google_workspace/test_google_drive.py b/app/tests/integrations/google_workspace/test_google_drive.py new file mode 100644 index 00000000..bd4aa580 --- /dev/null +++ b/app/tests/integrations/google_workspace/test_google_drive.py @@ -0,0 +1,375 @@ +"""Unit tests for google_drive module.""" +from unittest.mock import patch, call + +from integrations.google_workspace import google_drive + + +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_add_metadata_returns_result(execute_google_api_call_mock): + execute_google_api_call_mock.return_value = { + "name": "test_folder", + "appProperties": {"key": "value"}, + } + result = google_drive.add_metadata("file_id", "key", "value") + execute_google_api_call_mock.assert_called_once_with( + "drive", + "v3", + "files", + "update", + fileId="file_id", + body={"appProperties": {"key": "value"}}, + fields="name, appProperties", + supportsAllDrives=True, + ) + assert result == {"name": "test_folder", "appProperties": {"key": "value"}} + + +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_delete_metadata_returns_result(execute_google_api_call_mock): + execute_google_api_call_mock.return_value = { + "name": "test_folder", + "appProperties": {}, + } + result = google_drive.delete_metadata("file_id", "key") + execute_google_api_call_mock.assert_called_once_with( + "drive", + "v3", + "files", + "update", + fileId="file_id", + body={"appProperties": {"key": None}}, + fields="name, appProperties", + supportsAllDrives=True, + ) + assert result == {"name": "test_folder", "appProperties": {}} + + +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_list_metadata_returns_result(execute_google_api_call_mock): + execute_google_api_call_mock.return_value = { + "name": "test_folder", + "appProperties": {"key": "value"}, + } + result = google_drive.list_metadata("file_id") + execute_google_api_call_mock.assert_called_once_with( + "drive", + "v3", + "files", + "get", + fileId="file_id", + fields="id, name, appProperties", + supportsAllDrives=True, + ) + assert result == { + "name": "test_folder", + "appProperties": {"key": "value"}, + } + + +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_create_folder_returns_folder_id(execute_google_api_call_mock): + execute_google_api_call_mock.return_value = {"id": "test_folder_id"} + assert ( + google_drive.create_folder("test_folder", "parent_folder") == "test_folder_id" + ) + execute_google_api_call_mock.assert_called_once_with( + "drive", + "v3", + "files", + "create", + body={ + "name": "test_folder", + "mimeType": "application/vnd.google-apps.folder", + "parents": ["parent_folder"], + }, + supportsAllDrives=True, + fields="id", + ) + + +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_create_file_with_valid_type_returns_file_id(execute_google_api_call_mock): + execute_google_api_call_mock.return_value = {"id": "test_document_id"} + result = google_drive.create_file("test_document", "folder_id", "document") + assert result == "test_document_id" + execute_google_api_call_mock.assert_called_once_with( + "drive", + "v3", + "files", + "create", + body={ + "name": "test_document", + "mimeType": "application/vnd.google-apps.document", + "parents": ["folder_id"], + }, + supportsAllDrives=True, + fields="id", + ) + + +@patch("logging.error") +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_create_file_with_invalid_type_raises_value_error( + execute_google_api_call_mock, mocked_logging_error +): + execute_google_api_call_mock.side_effect = ValueError( + "Invalid file_type: invalid_file_type" + ) + 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.execute_google_api_call") +def test_create_file_from_template_returns_file_id(execute_google_api_call_mock): + execute_google_api_call_mock.return_value = {"id": "test_document_id"} + result = google_drive.create_file_from_template( + "test_document", "folder_id", "template_id" + ) + assert result == "test_document_id" + execute_google_api_call_mock.assert_called_once_with( + "drive", + "v3", + "files", + "copy", + fileId="template_id", + body={"name": "test_document", "parents": ["folder_id"]}, + supportsAllDrives=True, + fields="id", + ) + + +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_get_file_by_name_with_folder_id_returns_object(execute_google_api_call_mock): + execute_google_api_call_mock.return_value = [ + { + "name": "test_document", + "id": "test_document_id", + "appProperties": {}, + } + ] + result = google_drive.get_file_by_name("test_file_name", "folder_id") + assert result == [ + { + "name": "test_document", + "id": "test_document_id", + "appProperties": {}, + } + ] + execute_google_api_call_mock.assert_called_once_with( + "drive", + "v3", + "files", + "list", + scopes=["https://www.googleapis.com/auth/drive.readonly"], + paginate=True, + pageSize=1, + supportsAllDrives=True, + includeItemsFromAllDrives=True, + corpora="user", + q="trashed=false and name='test_file_name' and 'folder_id' in parents", + fields="files(appProperties, id, name)", + ) + + +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_get_file_by_name_without_folder_id_returns_object( + execute_google_api_call_mock, +): + execute_google_api_call_mock.return_value = [ + { + "name": "test_document", + "id": "test_document_id", + "appProperties": {}, + } + ] + result = google_drive.get_file_by_name("test_file_name") + assert result == [ + { + "name": "test_document", + "id": "test_document_id", + "appProperties": {}, + } + ] + execute_google_api_call_mock.assert_called_once_with( + "drive", + "v3", + "files", + "list", + scopes=["https://www.googleapis.com/auth/drive.readonly"], + paginate=True, + pageSize=1, + supportsAllDrives=True, + includeItemsFromAllDrives=True, + corpora="user", + q="trashed=false and name='test_file_name'", + fields="files(appProperties, id, name)", + ) + + +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_get_file_by_name_with_empty_folder_id_returns_object( + execute_google_api_call_mock, +): + execute_google_api_call_mock.return_value = [ + { + "name": "test_document", + "id": "test_document_id", + "appProperties": {}, + } + ] + result = google_drive.get_file_by_name("test_file_name", "") + assert result == [ + { + "name": "test_document", + "id": "test_document_id", + "appProperties": {}, + } + ] + execute_google_api_call_mock.assert_called_once_with( + "drive", + "v3", + "files", + "list", + scopes=["https://www.googleapis.com/auth/drive.readonly"], + paginate=True, + pageSize=1, + supportsAllDrives=True, + includeItemsFromAllDrives=True, + corpora="user", + q="trashed=false and name='test_file_name'", + fields="files(appProperties, id, name)", + ) + + +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_get_file_by_name_no_file_found_returns_empty_list( + execute_google_api_call_mock, +): + execute_google_api_call_mock.return_value = [] + result = google_drive.get_file_by_name("test_file_name", "folder_id") + assert result == [] + execute_google_api_call_mock.assert_called_once_with( + "drive", + "v3", + "files", + "list", + scopes=["https://www.googleapis.com/auth/drive.readonly"], + paginate=True, + pageSize=1, + supportsAllDrives=True, + includeItemsFromAllDrives=True, + corpora="user", + q="trashed=false and name='test_file_name' and 'folder_id' in parents", + fields="files(appProperties, id, name)", + ) + + +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_copy_file_to_folder_returns_file_id(execute_google_api_call_mock): + execute_google_api_call_mock.side_effect = [ + {"id": "file_id"}, # Response from the "copy" method + {"id": "updated_file_id"}, # Response from the "update" method + ] + assert ( + google_drive.copy_file_to_folder( + "file_id", "name", "parent_folder", "destination_folder" + ) + == "updated_file_id" + ) + execute_google_api_call_mock.assert_has_calls( + [ + call( + "drive", + "v3", + "files", + "copy", + fileId="file_id", + body={"name": "name", "parents": ["parent_folder"]}, + supportsAllDrives=True, + fields="id", + ), + call( + "drive", + "v3", + "files", + "update", + fileId="file_id", + addParents="destination_folder", + removeParents="parent_folder", + supportsAllDrives=True, + fields="id", + ), + ] + ) + + +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_list_folders_in_folder_returns_folders(execute_google_api_call_mock): + # Mock the results + results = [ + {"id": "test_folder_id", "name": "test_folder"}, + {"id": "test_folder_id2", "name": "test_folder2"}, + ] + + # Mock execute_google_api_call to return the results + execute_google_api_call_mock.return_value = results + + assert google_drive.list_folders_in_folder("parent_folder") == results + + execute_google_api_call_mock.assert_called_once_with( + "drive", + "v3", + "files", + "list", + paginate=True, + pageSize=25, + supportsAllDrives=True, + includeItemsFromAllDrives=True, + corpora="user", + q="parents in 'parent_folder' and mimeType = 'application/vnd.google-apps.folder' and trashed=false", + fields="files(id, name)", + ) + + +@patch("integrations.google_workspace.google_drive.execute_google_api_call") +def test_list_files_in_folder_returns_files(execute_google_api_call_mock): + # Mock the results + results = [ + {"id": "test_file_id", "name": "test_file"}, + {"id": "test_file_id2", "name": "test_file2"}, + ] + + # Mock execute_google_api_call to return the results + execute_google_api_call_mock.return_value = results + + assert google_drive.list_files_in_folder("parent_folder") == results + + execute_google_api_call_mock.assert_called_once_with( + "drive", + "v3", + "files", + "list", + paginate=True, + pageSize=25, + supportsAllDrives=True, + includeItemsFromAllDrives=True, + corpora="user", + q="parents in 'parent_folder' and mimeType != 'application/vnd.google-apps.folder' and trashed=false", + fields="files(id, name)", + ) + + +@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_drive_module.py b/app/tests/integrations/google_workspace/test_google_drive_module.py deleted file mode 100644 index 5d689601..00000000 --- a/app/tests/integrations/google_workspace/test_google_drive_module.py +++ /dev/null @@ -1,155 +0,0 @@ -"""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/intergrations/test_google_drive.py b/app/tests/intergrations/test_google_drive_module.py similarity index 99% rename from app/tests/intergrations/test_google_drive.py rename to app/tests/intergrations/test_google_drive_module.py index 56457399..4b5eb031 100644 --- a/app/tests/intergrations/test_google_drive.py +++ b/app/tests/intergrations/test_google_drive_module.py @@ -1,3 +1,4 @@ +"""NOTE: this module requires a suffix while the intergration/google_drive.py still exists. It should be removed after the integration/google_drive.py is properly refactored with the new google_workspace integration.""" import os import pytest