Skip to content

Commit 520cc33

Browse files
committed
refactor test
1 parent 547bab4 commit 520cc33

File tree

10 files changed

+187
-578
lines changed

10 files changed

+187
-578
lines changed

.github/workflows/pull_request.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ env:
88
BRANCH_NAME: ${{ github.event.pull_request.head.ref }}
99

1010
jobs:
11-
# quality_checks:
12-
# uses: NHSDigital/eps-workflow-quality-checks/.github/workflows/[email protected]
13-
# secrets:
14-
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
11+
quality_checks:
12+
uses: NHSDigital/eps-workflow-quality-checks/.github/workflows/[email protected]
13+
secrets:
14+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
1515

1616
pr_title_format_check:
1717
uses: ./.github/workflows/pr_title_check.yml
1818

1919
get_issue_number:
2020
runs-on: ubuntu-22.04
21+
needs: quality_checks
2122
outputs:
2223
issue_number: ${{steps.get_issue_number.outputs.result}}
2324

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ lint-flake8:
4646
poetry run flake8 .
4747

4848
test:
49-
cd packages/slackBotFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage python -m pytest
50-
cd packages/syncKnowledgeBaseFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage python -m pytest
49+
cd packages/slackBotFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage poetry run python -m pytest
50+
cd packages/syncKnowledgeBaseFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage poetry run python -m pytest
5151

5252
clean:
5353
rm -rf packages/cdk/coverage

packages/slackBotFunction/app/core/config.py

Lines changed: 66 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Sets up all the AWS and Slack connections we need.
44
"""
55

6+
from functools import lru_cache
67
import os
78
import json
89
import traceback
@@ -15,54 +16,68 @@
1516
# set up logging
1617
logger = Logger(service="slackBotFunction")
1718

18-
# DynamoDB table for deduplication and session storage
19-
dynamodb = boto3.resource("dynamodb")
20-
table = dynamodb.Table(os.environ["SLACK_BOT_STATE_TABLE"])
21-
22-
# get Slack credentials from Parameter Store
23-
bot_token_parameter = os.environ["SLACK_BOT_TOKEN_PARAMETER"]
24-
signing_secret_parameter = os.environ["SLACK_SIGNING_SECRET_PARAMETER"]
25-
26-
try:
27-
bot_token_raw = get_parameter(bot_token_parameter, decrypt=True)
28-
signing_secret_raw = get_parameter(signing_secret_parameter, decrypt=True)
29-
30-
if not bot_token_raw or not signing_secret_raw:
31-
raise ValueError("Missing required parameters from Parameter Store")
32-
33-
bot_token_data = json.loads(bot_token_raw)
34-
signing_secret_data = json.loads(signing_secret_raw)
35-
36-
bot_token = bot_token_data.get("token")
37-
signing_secret = signing_secret_data.get("secret")
38-
39-
if not bot_token or not signing_secret:
40-
raise ValueError("Missing required parameters: token or secret in Parameter Store values")
41-
42-
except json.JSONDecodeError as e:
43-
raise ValueError(f"Invalid JSON in Parameter Store: {e}")
44-
except Exception:
45-
logger.error("Configuration error", extra={"error": traceback.format_exc()})
46-
raise
47-
48-
# initialise the Slack app
49-
app = App(
50-
process_before_response=True,
51-
token=bot_token,
52-
signing_secret=signing_secret,
53-
)
54-
55-
# Bedrock configuration from environment
56-
KNOWLEDGEBASE_ID = os.environ["KNOWLEDGEBASE_ID"]
57-
RAG_MODEL_ID = os.environ["RAG_MODEL_ID"]
58-
AWS_REGION = os.environ["AWS_REGION"]
59-
GUARD_RAIL_ID = os.environ["GUARD_RAIL_ID"]
60-
GUARD_VERSION = os.environ["GUARD_RAIL_VERSION"]
61-
62-
logger.info("Guardrail configuration loaded", extra={"guardrail_id": GUARD_RAIL_ID, "guardrail_version": GUARD_VERSION})
63-
64-
# Bot response messages
65-
BOT_MESSAGES = {
66-
"empty_query": "Hi there! Please ask me a question and I'll help you find information from our knowledge base.",
67-
"error_response": "Sorry, an error occurred while processing your request. Please try again later.",
68-
}
19+
20+
@lru_cache()
21+
def get_slack_bot_state_table():
22+
# DynamoDB table for deduplication and session storage
23+
dynamodb = boto3.resource("dynamodb")
24+
return dynamodb.Table(os.environ["SLACK_BOT_STATE_TABLE"])
25+
26+
27+
@lru_cache()
28+
def get_app():
29+
# get Slack credentials from Parameter Store
30+
bot_token_parameter = os.environ["SLACK_BOT_TOKEN_PARAMETER"]
31+
signing_secret_parameter = os.environ["SLACK_SIGNING_SECRET_PARAMETER"]
32+
33+
try:
34+
bot_token_raw = get_parameter(bot_token_parameter, decrypt=True)
35+
signing_secret_raw = get_parameter(signing_secret_parameter, decrypt=True)
36+
37+
if not bot_token_raw or not signing_secret_raw:
38+
raise ValueError("Missing required parameters from Parameter Store")
39+
40+
bot_token_data = json.loads(bot_token_raw)
41+
signing_secret_data = json.loads(signing_secret_raw)
42+
43+
bot_token = bot_token_data.get("token")
44+
signing_secret = signing_secret_data.get("secret")
45+
46+
if not bot_token or not signing_secret:
47+
raise ValueError("Missing required parameters: token or secret in Parameter Store values")
48+
49+
except json.JSONDecodeError as e:
50+
raise ValueError(f"Invalid JSON in Parameter Store: {e}")
51+
except Exception:
52+
logger.error("Configuration error", extra={"error": traceback.format_exc()})
53+
raise
54+
55+
# initialise the Slack app
56+
app = App(
57+
process_before_response=True,
58+
token=bot_token,
59+
signing_secret=signing_secret,
60+
)
61+
return app, bot_token
62+
63+
64+
@lru_cache()
65+
def get_environment_variables():
66+
# Bedrock configuration from environment
67+
KNOWLEDGEBASE_ID = os.environ["KNOWLEDGEBASE_ID"]
68+
RAG_MODEL_ID = os.environ["RAG_MODEL_ID"]
69+
AWS_REGION = os.environ["AWS_REGION"]
70+
GUARD_RAIL_ID = os.environ["GUARD_RAIL_ID"]
71+
GUARD_VERSION = os.environ["GUARD_RAIL_VERSION"]
72+
73+
logger.info(
74+
"Guardrail configuration loaded", extra={"guardrail_id": GUARD_RAIL_ID, "guardrail_version": GUARD_VERSION}
75+
)
76+
77+
# Bot response messages
78+
BOT_MESSAGES = {
79+
"empty_query": "Hi there! Please ask me a question and I'll help you find information from our knowledge base.",
80+
"error_response": "Sorry, an error occurred while processing your request. Please try again later.",
81+
}
82+
83+
return KNOWLEDGEBASE_ID, RAG_MODEL_ID, AWS_REGION, GUARD_RAIL_ID, GUARD_VERSION, BOT_MESSAGES

packages/slackBotFunction/app/handler.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,10 @@
99
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
1010
from aws_lambda_powertools.utilities.typing import LambdaContext
1111

12-
from app.core.config import app, logger
12+
from app.core.config import get_app, logger
1313
from app.slack.slack_events import process_async_slack_event
1414
from app.slack.slack_handlers import setup_handlers
1515

16-
# register event handlers with the app
17-
setup_handlers(app)
18-
1916

2017
@logger.inject_lambda_context(log_event=True, clear_state=True)
2118
def handler(event: dict, context: LambdaContext) -> dict:
@@ -27,6 +24,10 @@ def handler(event: dict, context: LambdaContext) -> dict:
2724
2. Lambda acknowledges immediately and triggers async self-invocation
2825
3. Async invocation processes Bedrock query and responds to Slack
2926
"""
27+
# register event handlers with the app
28+
app, bot_token = get_app()
29+
setup_handlers(app)
30+
3031
logger.info("Lambda invoked", extra={"is_async": event.get("async_processing", False)})
3132

3233
# handle async processing requests

packages/slackBotFunction/app/slack/slack_events.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,9 @@
99
import boto3
1010
from slack_sdk import WebClient
1111
from app.core.config import (
12-
table,
12+
get_environment_variables,
13+
get_slack_bot_state_table,
1314
logger,
14-
KNOWLEDGEBASE_ID,
15-
RAG_MODEL_ID,
16-
AWS_REGION,
17-
GUARD_RAIL_ID,
18-
GUARD_VERSION,
19-
BOT_MESSAGES,
2015
)
2116
from app.services.query_reformulator import reformulate_query
2217

@@ -31,6 +26,7 @@ def process_async_slack_event(slack_event_data):
3126
event = slack_event_data["event"]
3227
event_id = slack_event_data["event_id"]
3328
token = slack_event_data["bot_token"]
29+
KNOWLEDGEBASE_ID, RAG_MODEL_ID, AWS_REGION, GUARD_RAIL_ID, GUARD_VERSION, BOT_MESSAGES = get_environment_variables()
3430

3531
client = WebClient(token=token)
3632

@@ -106,7 +102,8 @@ def get_conversation_session(conversation_key):
106102
Get existing Bedrock session for this conversation
107103
"""
108104
try:
109-
response = table.get_item(Key={"pk": conversation_key, "sk": "session"})
105+
slack_bot_state_table = get_slack_bot_state_table()
106+
response = slack_bot_state_table.get_item(Key={"pk": conversation_key, "sk": "session"})
110107
if "Item" in response:
111108
logger.info("Found existing session", extra={"conversation_key": conversation_key})
112109
return response["Item"]["session_id"]
@@ -122,7 +119,8 @@ def store_conversation_session(conversation_key, session_id, user_id, channel_id
122119
"""
123120
try:
124121
ttl = int(time.time()) + 2592000 # 30 days
125-
table.put_item(
122+
slack_bot_state_table = get_slack_bot_state_table()
123+
slack_bot_state_table.put_item(
126124
Item={
127125
"pk": conversation_key,
128126
"sk": "session",
@@ -147,6 +145,7 @@ def query_bedrock(user_query, session_id=None):
147145
a response using the configured LLM model with guardrails for safety.
148146
"""
149147

148+
KNOWLEDGEBASE_ID, RAG_MODEL_ID, AWS_REGION, GUARD_RAIL_ID, GUARD_VERSION, BOT_MESSAGES = get_environment_variables()
150149
client = boto3.client(
151150
service_name="bedrock-agent-runtime",
152151
region_name=AWS_REGION,

packages/slackBotFunction/app/slack/slack_handlers.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
Slack event handlers - handles @mentions and direct messages to the bot
33
"""
44

5+
from functools import lru_cache
56
import time
67
import json
78
import traceback
89
import boto3
910
from botocore.exceptions import ClientError
10-
from app.core.config import table, bot_token, logger
11+
from slack_bolt import App
12+
from app.core.config import get_app, get_slack_bot_state_table, logger
1113
import os
1214

1315

14-
def setup_handlers(app):
16+
@lru_cache()
17+
def setup_handlers(app: App):
1518
"""
1619
Register all event handlers with the Slack app
1720
"""
@@ -37,6 +40,7 @@ def handle_app_mention(event, ack, body):
3740
user_id = event.get("user", "unknown")
3841
logger.info("Processing @mention from user", extra={"user_id": user_id, "event_id": event_id})
3942

43+
app, bot_token = get_app()
4044
trigger_async_processing({"event": event, "event_id": event_id, "bot_token": bot_token})
4145

4246
@app.event("message")
@@ -59,6 +63,7 @@ def handle_direct_message(event, ack, body):
5963
user_id = event.get("user", "unknown")
6064
logger.info("Processing DM from user", extra={"user_id": user_id, "event_id": event_id})
6165

66+
app, bot_token = get_app()
6267
trigger_async_processing({"event": event, "event_id": event_id, "bot_token": bot_token})
6368

6469

@@ -71,7 +76,8 @@ def is_duplicate_event(event_id):
7176
"""
7277
try:
7378
ttl = int(time.time()) + 3600 # 1 hour TTL
74-
table.put_item(
79+
slack_bot_state_table = get_slack_bot_state_table()
80+
slack_bot_state_table.put_item(
7581
Item={"pk": f"event#{event_id}", "sk": "dedup", "ttl": ttl, "timestamp": int(time.time())},
7682
ConditionExpression="attribute_not_exists(pk)",
7783
)

packages/slackBotFunction/tests/conftest.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1+
import json
12
import pytest
23

3-
from unittest.mock import Mock, patch
4-
from moto import mock_aws
5-
import boto3
4+
from unittest.mock import MagicMock, Mock, patch
65
import os
76

87

@@ -29,24 +28,38 @@ def mock_env():
2928
yield env_vars
3029

3130

32-
@pytest.fixture
33-
def mock_dynamodb_table():
34-
"""Mock DynamoDB table"""
35-
with mock_aws():
36-
dynamodb = boto3.resource("dynamodb")
37-
table = dynamodb.create_table(
38-
TableName="test-bot-state-table",
39-
KeySchema=[{"AttributeName": "eventId", "KeyType": "HASH"}],
40-
AttributeDefinitions=[{"AttributeName": "eventId", "AttributeType": "S"}],
41-
BillingMode="PAY_PER_REQUEST",
42-
)
43-
yield table
44-
45-
4631
@pytest.fixture
4732
def lambda_context():
4833
"""Mock Lambda context"""
4934
context = Mock()
5035
context.function_name = "test-function"
5136
context.aws_request_id = "test-request-id"
5237
return context
38+
39+
40+
@pytest.fixture
41+
def mock_get_parameter():
42+
def fake_get_parameter(name, *args, **kwargs):
43+
return {
44+
"/test/bot-token": json.dumps({"token": "test-token"}),
45+
"/test/signing-secret": json.dumps({"secret": "test-secret"}),
46+
}[name]
47+
48+
with patch("app.core.config.get_parameter", side_effect=fake_get_parameter) as mock:
49+
yield mock
50+
51+
52+
@pytest.fixture
53+
def mock_slack_app():
54+
with patch("app.core.config.App") as mock_app_cls:
55+
mock_instance = MagicMock()
56+
mock_app_cls.return_value = mock_instance
57+
yield mock_instance
58+
59+
60+
@pytest.fixture
61+
def mock_table():
62+
with patch("app.core.config.get_slack_bot_state_table") as mock_func:
63+
fake_table = MagicMock()
64+
mock_func.return_value = fake_table
65+
yield fake_table

0 commit comments

Comments
 (0)