Skip to content

Commit

Permalink
Refactor/google workspace service (#388)
Browse files Browse the repository at this point in the history
* feat: add google meet integration

* fix: remove typo

* fix: delete file due to conflict with separate dev branch

* fix: fmt

* feat: setup standalone google_service function

* feat: add debug config file

* fix: use json format for service account credentials

* tmp: setup test command using new service

* fix: devcontainer customization errors

* test: updated devcontainer

* fix: lint & fmt
  • Loading branch information
gcharest authored Feb 1, 2024
1 parent 1847913 commit 754d476
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 36 deletions.
59 changes: 28 additions & 31 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,36 @@
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
// Set *default* container specific settings.json values on container create.
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.languageServer": "Pylance",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
"[python]": {
"editor.formatOnSave": true
},
"[terraform]": {
"editor.formatOnSave": true
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.flake8",
"ms-python.pylint",
"ms-python.mypy-type-checker",
"mtxr.sqltools",
"mtxr.sqltools-driver-pg",
"redhat.vscode-yaml",
"timonwong.shellcheck",
"hashicorp.terraform",
"github.copilot"
],
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.languageServer": "Pylance",
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"[python]": {
"editor.formatOnSave": true
},
"[terraform]": {
"editor.formatOnSave": true
}
}
}
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"mtxr.sqltools",
"mtxr.sqltools-driver-pg",
"redhat.vscode-yaml",
"timonwong.shellcheck",
"hashicorp.terraform",
"github.copilot"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
Expand Down
21 changes: 21 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Module",
"type": "python",
"request": "launch",
"module": "uvicorn",
"args": [
"main:server_app",
"--reload"
],
"env": {
"LOGLEVEL": "DEBUG",
"PREFIX": "dev-"
},
"console": "integratedTerminal",
"cwd": "${workspaceFolder}/app"
}
]
}
4 changes: 1 addition & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"python.linting.enabled": true
"python.pylintPath": "/usr/local/py-utils/bin/pylint",
}
49 changes: 49 additions & 0 deletions app/commands/google_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Testing new google service (will be removed)"""
import os

from integrations.google_workspace.google_service import get_google_service
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")


def open_modal(client, body):
folders = list_folders()
folder_names = [i["name"] for i in folders]
blocks = [
{"type": "section", "text": {"type": "mrkdwn", "text": f"*{name}*"}}
for name in folder_names
]
view = {
"type": "modal",
"title": {"type": "plain_text", "text": "Folder List"},
"blocks": blocks,
}
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", [])
4 changes: 3 additions & 1 deletion app/commands/sre.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os

from commands import utils
from commands import utils, google_service

from commands.helpers import geolocate_helper, incident_helper, webhook_helper

Expand Down Expand Up @@ -47,6 +47,8 @@ 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 _:
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"
Expand Down
Empty file.
Empty file.
30 changes: 30 additions & 0 deletions app/integrations/google_workspace/google_meet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Google Meet integration."""
import re
from datetime import datetime


def create_google_meet(title=None):
"""
Starts a Google Meet session.
Args:
title (str, optional): The title of the session.
Returns:
str: The URL of the Google Meet session.
"""
# if title is None, set it to the current date
if title is None:
title = f"Meeting-Rencontre-{datetime.now().strftime('%Y-%m-%d')}"

# replace spaces with dashes
title = title.replace(" ", "-")
# remove any special characters
title = re.sub("[^0-9a-zA-Z]+", "-", title)
title = title.strip("-")

meeting_link = f"https://g.co/meet/{title}"
if len(meeting_link) > 78:
meeting_link = meeting_link[:78]

return meeting_link
39 changes: 39 additions & 0 deletions app/integrations/google_workspace/google_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Google Service Module."""
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

load_dotenv()


def get_google_service(service, version):
"""
Get an authenticated Google service.
Args:
service (str): The Google service to get.
version (str): The version of the service to get.
Returns:
The authenticated Google service resource.
"""

creds_json = os.environ.get("GCP_SRE_SERVICE_ACCOUNT_KEY_FILE", False)

if creds_json is False:
raise ValueError("Credentials JSON not set")

try:
creds_info = json.loads(creds_json)
creds = service_account.Credentials.from_service_account_info(creds_info)
except JSONDecodeError as json_decode_exception:
logging.error("Error while loading credentials JSON: %s", json_decode_exception)
raise JSONDecodeError(
msg="Invalid credentials JSON", doc="Credentials JSON", pos=0
) from json_decode_exception
return build(service, version, credentials=creds, cache_discovery=False)
6 changes: 5 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from slack_bolt.adapter.socket_mode import SocketModeHandler
from slack_bolt import App
from dotenv import load_dotenv
from commands import atip, aws, incident, secret, sre, role
from commands import atip, aws, incident, secret, sre, role, google_service
from commands.helpers import incident_helper, webhook_helper
from server import bot_middleware, server

Expand All @@ -26,6 +26,10 @@ 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)
Expand Down
73 changes: 73 additions & 0 deletions app/tests/integrations/google_workspace/test_google_meet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Test google_meet.py functions."""
from unittest.mock import patch
from integrations.google_workspace import google_meet


def test_create_google_meet_with_valid_title():
"""Test create_google_meet with a title."""
title = "TestTitle"
expected = "https://g.co/meet/TestTitle"
assert google_meet.create_google_meet(title) == expected


@patch("integrations.google_workspace.google_meet.datetime")
def test_create_google_meet_without_title(date_time_mock):
"""Test create_google_meet without a title."""
date_time_mock.now.return_value.strftime.return_value = "2021-06-01"
expected = "https://g.co/meet/Meeting-Rencontre-2021-06-01"
assert google_meet.create_google_meet() == expected


def test_create_google_meet_with_title_too_long():
"""Test create_google_meet with a title that is too long."""
title = (
"Testing-title-that-is-much-too-long-for-google-meet-and"
"-it-should-be-truncated"
)
expected = (
"https://g.co/meet/Testing-title-that-is-much-too-long-for-google-meet-and-it-s"
)
assert google_meet.create_google_meet(title) == expected


def test_create_google_meet_with_title_with_spaces():
"""Test create_google_meet with a title that has spaces."""
title = "Testing title with spaces"
expected = "https://g.co/meet/Testing-title-with-spaces"
assert google_meet.create_google_meet(title) == expected


def test_create_google_meet_with_title_too_long_and_spaces():
"""Test create_google_meet with a title that is too long and has spaces."""
title = (
"Testing title that is much too long for google"
" meet and it should be truncated"
)
expected = (
"https://g.co/meet/Testing-title-that-is-much-too-"
"long-for-google-meet-and-it-s"
)
assert google_meet.create_google_meet(title) == expected
assert len(google_meet.create_google_meet(title)) == 78


def test_create_google_meet_with_title_special_characters():
"""Test create_google_meet with a title that has special characters."""
title = "Testing title with special @!@#$!@# characters !@#$%^&*()"
expected = "https://g.co/meet/Testing-title-with-special-characters"
assert google_meet.create_google_meet(title) == expected


def test_create_google_meet_with_title_too_long_and_special_characters():
"""Test create_google_meet with a title that is too long
and has special characters."""
title = (
"Testing title that is much too long for google"
" meet and it should be truncated !@#$%^&*()"
)
expected = (
"https://g.co/meet/Testing-title-that-is-much-too-"
"long-for-google-meet-and-it-s"
)
assert google_meet.create_google_meet(title) == expected
assert len(google_meet.create_google_meet(title)) == 78
52 changes: 52 additions & 0 deletions app/tests/integrations/google_workspace/test_google_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import json
import pytest
from unittest.mock import patch, MagicMock
from integrations.google_workspace.google_service import get_google_service
from json import JSONDecodeError


@patch("integrations.google_workspace.google_service.build")
@patch(
"integrations.google_workspace.google_service.service_account.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.
"""
credentials_mock.return_value = MagicMock()
with patch.dict(
"os.environ",
{"GCP_SRE_SERVICE_ACCOUNT_KEY_FILE": json.dumps({"type": "service_account"})},
):
get_google_service("drive", "v3")
build_mock.assert_called_once_with(
"drive", "v3", credentials=credentials_mock.return_value, cache_discovery=False
)


def test_get_google_service_raises_exception_if_credentials_json_not_set():
"""
Test case to verify that the function raises an exception if:
- GCP_SRE_SERVICE_ACCOUNT_KEY_FILE is not set.
"""
with patch.dict("os.environ", clear=True):
with pytest.raises(ValueError) as e:
get_google_service("drive", "v3")
assert "Credentials JSON not set" in str(e.value)


@patch("integrations.google_workspace.google_service.build")
@patch(
"integrations.google_workspace.google_service.service_account.Credentials.from_service_account_info"
)
def test_get_google_service_raises_exception_if_credentials_json_is_invalid(
credentials_mock, build_mock
):
"""
Test case to verify that the function raises an exception if:
- GCP_SRE_SERVICE_ACCOUNT_KEY_FILE is invalid.
"""
with patch.dict("os.environ", {"GCP_SRE_SERVICE_ACCOUNT_KEY_FILE": "invalid"}):
with pytest.raises(JSONDecodeError) as e:
get_google_service("drive", "v3")
assert "Invalid credentials JSON" in str(e.value)

0 comments on commit 754d476

Please sign in to comment.