Skip to content

Commit

Permalink
use a global context
Browse files Browse the repository at this point in the history
  • Loading branch information
WilliamBergamin committed May 30, 2024
1 parent 027d36b commit 46710f6
Show file tree
Hide file tree
Showing 12 changed files with 136 additions and 59 deletions.
21 changes: 9 additions & 12 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@
from slack_bolt.adapter.socket_mode import SocketModeHandler

from jira.client import JiraClient
from jira.oauth.installation_store.file import JiraFileInstallationStore
from jira.oauth.state_store.file import JiraFileOAuthStateStore
from listeners import register_listeners
from utils.constants import JIRA_CODE_VERIFIER, OAUTH_REDIRECT_PATH
from utils.env_variables import APP_HOME_PAGE_URL, JIRA_CLIENT_ID, JIRA_CLIENT_SECRET, JIRA_REDIRECT_URI
from utils.constants import CONTEXT

logging.basicConfig(level=logging.INFO)

Expand All @@ -22,28 +19,28 @@
register_listeners(app)


@flask_app.route(OAUTH_REDIRECT_PATH, methods=["GET"])
@flask_app.route(CONTEXT.jira_oauth_redirect_path, methods=["GET"])
def oauth_redirect():
code = request.args["code"]
state = request.args["state"]

jira_client = JiraClient()
jira_resp = jira_client.oauth2_token(
code=code,
client_id=JIRA_CLIENT_ID,
client_secret=JIRA_CLIENT_SECRET,
code_verifier=JIRA_CODE_VERIFIER,
redirect_uri=JIRA_REDIRECT_URI,
client_id=CONTEXT.jira_client_id,
client_secret=CONTEXT.jira_client_secret,
code_verifier=CONTEXT.jira_code_verifier,
redirect_uri=CONTEXT.jira_redirect_uri,
)
jira_resp.raise_for_status()
jira_resp_json = jira_resp.json()

user_identity = JiraFileOAuthStateStore().consume(state)
user_identity = CONTEXT.jira_state_store.consume(state)

if user_identity is None:
return make_response("State Not Found", 404)

JiraFileInstallationStore().save(
CONTEXT.jira_installation_store.save(
{
"access_token": jira_resp_json["access_token"],
"enterprise_id": user_identity["enterprise_id"],
Expand All @@ -56,7 +53,7 @@ def oauth_redirect():
"user_id": user_identity["user_id"],
}
)
return redirect(APP_HOME_PAGE_URL, code=302)
return redirect(CONTEXT.app_home_page_url, code=302)


if __name__ == "__main__":
Expand Down
4 changes: 3 additions & 1 deletion jira/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import requests
from requests import Response

from utils.constants import CONTEXT


class JiraClient:
def __init__(
Expand All @@ -17,7 +19,7 @@ def __init__(
proxies: Optional[Dict[str, str]] = None,
):
self.token = token
self.base_url = base_url or os.environ.get("JIRA_BASE_URL")
self.base_url = base_url or CONTEXT.jira_base_url
self.token_type = token_type
self.headers = headers or {}
self.headers[os.getenv("SECRET_HEADER_KEY")] = os.getenv("SECRET_HEADER_VALUE")
Expand Down
4 changes: 2 additions & 2 deletions listeners/actions/disconnect_account.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from slack_bolt import Ack, BoltContext

from jira.oauth.installation_store.file import JiraFileInstallationStore
from utils.constants import CONTEXT


def disconnect_account_callback(ack: Ack, context: BoltContext):
ack()
JiraFileInstallationStore().delete_installation(
CONTEXT.jira_installation_store.delete_installation(
enterprise_id=context.enterprise_id, team_id=context.team_id, user_id=context.user_id
)
15 changes: 6 additions & 9 deletions listeners/events/app_home/app_home_open.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
from slack_sdk import WebClient

from jira.client import JiraClient
from jira.oauth.installation_store.file import JiraFileInstallationStore
from jira.oauth.state_store.file import JiraFileOAuthStateStore
from jira.oauth.state_store.models import JiraUserIdentity
from utils.constants import JIRA_CODE_VERIFIER
from utils.env_variables import JIRA_CLIENT_ID, JIRA_REDIRECT_URI
from utils.constants import CONTEXT

from .builder import AppHomeBuilder

Expand All @@ -19,22 +16,22 @@ def app_home_open_callback(client: WebClient, event: dict, logger: Logger, conte
return
try:
home = AppHomeBuilder()
installation = JiraFileInstallationStore().find_installation(
installation = CONTEXT.jira_installation_store.find_installation(
user_id=context.user_id, team_id=context.team_id, enterprise_id=context.enterprise_id
)

if installation is None:
state = JiraFileOAuthStateStore().issue(
state = CONTEXT.jira_state_store.issue(
user_identity=JiraUserIdentity(
user_id=context.user_id, team_id=context.team_id, enterprise_id=context.enterprise_id
)
)
jira_client = JiraClient()
authorization_url = jira_client.build_authorization_url(
client_id=JIRA_CLIENT_ID,
redirect_uri=JIRA_REDIRECT_URI,
client_id=CONTEXT.jira_client_id,
redirect_uri=CONTEXT.jira_redirect_uri,
scope="WRITE",
code_challenge=JIRA_CODE_VERIFIER,
code_challenge=CONTEXT.jira_code_verifier,
state=state,
)
home.add_connect_account_button(authorization_url)
Expand Down
23 changes: 19 additions & 4 deletions listeners/functions/create_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from slack_sdk import WebClient

from jira.client import JiraClient
from jira.oauth.installation_store.file import JiraFileInstallationStore
from utils.constants import CONTEXT


# https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/
Expand All @@ -16,15 +16,30 @@ def create_issue_callback(
ack()
user_id = inputs["user_context"]["id"]

installation = JiraFileInstallationStore().find_installation(
installation = CONTEXT.jira_installation_store.find_installation(
user_id=user_id, team_id=context.team_id, enterprise_id=context.enterprise_id
)
if installation is None:
mrkdwn: str = (
":no_entry: There is a problem with the `Create an issue` step, you did not connect a Jira Account, "
f"visit the <{CONTEXT.app_home_page_url}|App Home Page> to fix this!"
)
client.chat_postMessage(
channel=user_id,
text="The function failed because the is no connected jira account, visit the app home to solve this",
text=mrkdwn,
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": mrkdwn,
},
}
],
)
return fail(
f"User {user_id} has not connected their account properly, they must visit the App Home Page to fix this"
)
return fail(f"User {user_id} has not connected their account properly, visit the app home to solve this")

try:
project: str = inputs["project"]
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"display_name": "BoltPy Jira Functions"
},
"app_home": {
"messages_tab_enabled": false,
"messages_tab_enabled": true,
"home_tab_enabled": true
}
},
Expand Down
32 changes: 14 additions & 18 deletions tests/functions/test_create_issue.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import json
import logging
import os
from datetime import datetime
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, Mock, patch

import requests
from slack_sdk import WebClient

from listeners.functions.create_issue import create_issue_callback
from tests.mock_jira_installation_store import MockJiraInstallationStore
from tests.utils import remove_os_env_temporarily, restore_os_env
from tests.utils import build_mock_context, remove_os_env_temporarily, restore_os_env


def mock_response(status=200, data: dict = None):
Expand All @@ -24,9 +23,10 @@ class TestCreateIssue:
team_id = "T1234"
enterprise_id = "E1234"

def build_mock_installation_store(self):
installation_store = MockJiraInstallationStore()
installation_store.save(
def setup_method(self):
self.old_os_env = remove_os_env_temporarily()
self.mock_context = build_mock_context()
self.mock_context.jira_installation_store.save(
{
"scope": "WRITE",
"access_token": "jira_access_token",
Expand All @@ -39,18 +39,14 @@ def build_mock_installation_store(self):
"installed_at": datetime.now().timestamp(),
}
)
return installation_store

def setup_method(self):
self.old_os_env = remove_os_env_temporarily()
os.environ["JIRA_BASE_URL"] = "https://jira-dev/"
self.mock_installation_store = patch(
"listeners.functions.create_issue.JiraFileInstallationStore", self.build_mock_installation_store
)
self.mock_installation_store.start()
self.mock_create_issue_context = patch("listeners.functions.create_issue.CONTEXT", self.mock_context)
self.mock_jira_client_context = patch("jira.client.CONTEXT", self.mock_context)
self.mock_jira_client_context.start()
self.mock_create_issue_context.start()

def teardown_method(self):
self.mock_installation_store.stop()
self.mock_create_issue_context.stop()
self.mock_jira_client_context.stop()
restore_os_env(self.old_os_env)

def test_create_issue(self):
Expand Down Expand Up @@ -97,7 +93,7 @@ def test_create_issue_fail(self):
mock_fail = MagicMock()
mock_complete = MagicMock()
mock_context = MagicMock(team_id=self.team_id, enterprise_id=self.enterprise_id)
mock_client = MagicMock(chat_postMessage=lambda channel, text: True)
mock_client = Mock(spec=WebClient)
mock_inputs = {
"user_context": {"id": "wrong_id"},
"project": "PROJ",
Expand Down
28 changes: 28 additions & 0 deletions tests/mock_jira_oauth_state_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import logging
import uuid
from logging import Logger
from typing import Dict, Optional

from jira.oauth.state_store.models import JiraUserIdentity
from jira.oauth.state_store.state_store import JiraOAuthStateStore


class MockJiraOAuthStateStore(JiraOAuthStateStore):
def __init__(
self,
*,
logger: Logger = logging.getLogger(__name__),
):
self.state_table: Dict[str, JiraUserIdentity] = {}
self.logger = logger

def issue(self, user_identity: JiraUserIdentity) -> str:
state = uuid.uuid4().hex
self.state_table[state] = user_identity
return state

def consume(self, state: str) -> Optional[JiraUserIdentity]:
if state not in self.state_table:
message = f"Failed to find any persistent data for state: {state} - Key Error"
self.logger.warning(message)
return self.state_table.pop(state, None)
16 changes: 16 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import os

from tests.mock_jira_installation_store import MockJiraInstallationStore
from tests.mock_jira_oauth_state_store import MockJiraOAuthStateStore
from utils.context import Context


def remove_os_env_temporarily() -> dict:
old_env = os.environ.copy()
Expand All @@ -9,3 +13,15 @@ def remove_os_env_temporarily() -> dict:

def restore_os_env(old_env: dict) -> None:
os.environ.update(old_env)


def build_mock_context() -> Context:
return Context(
jira_base_url="https://jira-dev/",
jira_client_id="abc123_id",
jira_client_secret="abc123_secret",
app_base_url="http://127.0.0.1:3000",
app_home_page_url="slack://app?team=T123&id=A123&tab=home",
jira_installation_store=MockJiraInstallationStore(),
jira_state_store=MockJiraOAuthStateStore(),
)
5 changes: 2 additions & 3 deletions utils/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import secrets
from utils.context import Context

OAUTH_REDIRECT_PATH = "/oauth/redirect"
JIRA_CODE_VERIFIER = secrets.token_urlsafe(96)[:128]
CONTEXT = Context()
36 changes: 36 additions & 0 deletions utils/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import os
import secrets
from typing import Optional
from urllib.parse import urljoin

from jira.oauth.installation_store.file import JiraFileInstallationStore
from jira.oauth.installation_store.installation_store import JiraInstallationStore
from jira.oauth.state_store.file import JiraFileOAuthStateStore
from jira.oauth.state_store.state_store import JiraOAuthStateStore


class Context:
def __init__(
self,
jira_base_url: Optional[str] = None,
jira_client_id: Optional[str] = None,
jira_client_secret: Optional[str] = None,
jira_code_verifier: Optional[str] = None,
app_base_url: Optional[str] = None,
jira_oauth_redirect_path: str = "/oauth/redirect",
app_home_page_url: Optional[str] = None,
jira_state_store: JiraOAuthStateStore = JiraFileOAuthStateStore(),
jira_installation_store: JiraInstallationStore = JiraFileInstallationStore(),
):
self.jira_base_url = jira_base_url or os.getenv("JIRA_BASE_URL")
self.jira_client_id = jira_client_id or os.getenv("JIRA_CLIENT_ID")
self.jira_client_secret = jira_client_secret or os.getenv("JIRA_CLIENT_SECRET")
self.app_base_url = app_base_url or os.getenv("APP_BASE_URL")
self.app_home_page_url = app_home_page_url or os.getenv("APP_HOME_PAGE_URL")

self.jira_code_verifier = jira_code_verifier or secrets.token_urlsafe(96)[:128]
self.jira_oauth_redirect_path = jira_oauth_redirect_path
self.jira_redirect_uri = urljoin(self.app_base_url, self.jira_oauth_redirect_path)

self.jira_state_store = jira_state_store
self.jira_installation_store = jira_installation_store
9 changes: 0 additions & 9 deletions utils/env_variables.py

This file was deleted.

0 comments on commit 46710f6

Please sign in to comment.