mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-04-05 06:52:42 +00:00
Compare commits
4 Commits
v3.1.0-clo
...
tests/slac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1845c1c9eb | ||
|
|
5be04da2dd | ||
|
|
3a0d4967c0 | ||
|
|
43163582ae |
@@ -133,7 +133,9 @@ def get_channel_messages(
|
||||
) -> Generator[list[MessageType], None, None]:
|
||||
"""Get all messages in a channel"""
|
||||
# join so that the bot can access messages
|
||||
if not channel["is_member"]:
|
||||
#if is_member is not available, it means the channel is dm, so we assume bot is a member
|
||||
is_member = channel.get("is_member", True)
|
||||
if not is_member:
|
||||
make_slack_api_call_w_retries(
|
||||
client.conversations_join,
|
||||
channel=channel["id"],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
@@ -224,7 +223,7 @@ class SlackbotHandler:
|
||||
f"No Slack bot tokens found for tenant={tenant_id}, bot {bot.id}"
|
||||
)
|
||||
if tenant_bot_pair in self.socket_clients:
|
||||
asyncio.run(self.socket_clients[tenant_bot_pair].close())
|
||||
self.socket_clients[tenant_bot_pair].close()
|
||||
del self.socket_clients[tenant_bot_pair]
|
||||
del self.slack_bot_tokens[tenant_bot_pair]
|
||||
return
|
||||
@@ -252,7 +251,7 @@ class SlackbotHandler:
|
||||
|
||||
# Close any existing connection first
|
||||
if tenant_bot_pair in self.socket_clients:
|
||||
asyncio.run(self.socket_clients[tenant_bot_pair].close())
|
||||
self.socket_clients[tenant_bot_pair].close()
|
||||
|
||||
self.start_socket_client(bot.id, tenant_id, slack_bot_tokens)
|
||||
|
||||
@@ -405,7 +404,7 @@ class SlackbotHandler:
|
||||
# Close all socket clients for this tenant
|
||||
for (t_id, slack_bot_id), client in list(self.socket_clients.items()):
|
||||
if t_id == tenant_id:
|
||||
asyncio.run(client.close())
|
||||
client.close()
|
||||
del self.socket_clients[(t_id, slack_bot_id)]
|
||||
del self.slack_bot_tokens[(t_id, slack_bot_id)]
|
||||
logger.info(
|
||||
@@ -484,7 +483,7 @@ class SlackbotHandler:
|
||||
def stop_socket_clients(self) -> None:
|
||||
logger.info(f"Stopping {len(self.socket_clients)} socket clients")
|
||||
for (tenant_id, slack_bot_id), client in list(self.socket_clients.items()):
|
||||
asyncio.run(client.close())
|
||||
client.close()
|
||||
logger.info(
|
||||
f"Stopped SocketModeClient for tenant: {tenant_id}, app: {slack_bot_id}"
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ pre-commit==3.2.2
|
||||
pytest-asyncio==0.22.0
|
||||
pytest-xdist==3.6.1
|
||||
pytest==8.3.5
|
||||
pytest-order==1.3.0
|
||||
reorder-python-imports-black==3.14.0
|
||||
ruff==0.0.286
|
||||
sentence-transformers==4.0.2
|
||||
|
||||
632
backend/tests/integration/common_utils/managers/slack.py
Normal file
632
backend/tests/integration/common_utils/managers/slack.py
Normal file
@@ -0,0 +1,632 @@
|
||||
"""
|
||||
Assumptions:
|
||||
- The test users have already been created
|
||||
- General is empty of messages
|
||||
- In addition to the normal slack oauth permissions, the following scopes are needed:
|
||||
- channels:manage
|
||||
- chat:write.public
|
||||
- channels:history
|
||||
- channels:write
|
||||
- chat:write
|
||||
- groups:history
|
||||
- groups:write
|
||||
- im:history
|
||||
- im:write
|
||||
- mpim:history
|
||||
- mpim:write
|
||||
- users:write
|
||||
- channels:read
|
||||
- groups:read
|
||||
- mpim:read
|
||||
- im:read
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from slack_sdk import WebClient
|
||||
from slack_sdk.errors import SlackApiError
|
||||
|
||||
from onyx.connectors.slack.connector import default_msg_filter
|
||||
from onyx.connectors.slack.connector import get_channel_messages
|
||||
from onyx.connectors.slack.utils import make_paginated_slack_api_call_w_retries
|
||||
from onyx.connectors.slack.utils import make_slack_api_call_w_retries
|
||||
from onyx.utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def _get_slack_channel_id(channel: dict[str, Any]) -> str:
|
||||
if not (channel_id := channel.get("id")):
|
||||
raise ValueError("Channel ID is missing")
|
||||
return channel_id
|
||||
|
||||
|
||||
def _get_non_general_channels(
|
||||
slack_client: WebClient,
|
||||
get_private: bool,
|
||||
get_public: bool,
|
||||
only_get_done: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
channel_types = []
|
||||
if get_private:
|
||||
channel_types.append("private_channel")
|
||||
if get_public:
|
||||
channel_types.append("public_channel")
|
||||
|
||||
conversations: list[dict[str, Any]] = []
|
||||
for result in make_paginated_slack_api_call_w_retries(
|
||||
slack_client.conversations_list,
|
||||
exclude_archived=False,
|
||||
types=channel_types,
|
||||
):
|
||||
conversations.extend(result["channels"])
|
||||
|
||||
filtered_conversations = []
|
||||
for conversation in conversations:
|
||||
if conversation.get("is_general", False):
|
||||
continue
|
||||
if only_get_done and "done" not in conversation.get("name", ""):
|
||||
continue
|
||||
filtered_conversations.append(conversation)
|
||||
return filtered_conversations
|
||||
|
||||
|
||||
def _clear_slack_conversation_members(
|
||||
slack_client: WebClient,
|
||||
admin_user_id: str,
|
||||
channel: dict[str, Any],
|
||||
) -> None:
|
||||
channel_id = _get_slack_channel_id(channel)
|
||||
member_ids: list[str] = []
|
||||
for result in make_paginated_slack_api_call_w_retries(
|
||||
slack_client.conversations_members,
|
||||
channel=channel_id,
|
||||
):
|
||||
member_ids.extend(result["members"])
|
||||
|
||||
for member_id in member_ids:
|
||||
if member_id == admin_user_id:
|
||||
continue
|
||||
try:
|
||||
slack_client.conversations_kick(channel=channel_id, user=member_id)
|
||||
logger.info(f"Kicked member {member_id} from channel {channel_id}")
|
||||
except Exception as e:
|
||||
if "cant_kick_self" in str(e):
|
||||
continue
|
||||
logger.error(
|
||||
f"Error kicking member {member_id} from channel {channel_id}: {e}"
|
||||
)
|
||||
logger.error(f"Failed member ID: {member_id}")
|
||||
try:
|
||||
slack_client.conversations_unarchive(channel=channel_id)
|
||||
logger.info(f"Unarchived channel {channel_id}")
|
||||
channel["is_archived"] = False
|
||||
except Exception as e:
|
||||
# Channel is already unarchived or another error occurred
|
||||
logger.warning(
|
||||
f"Could not unarchive channel {channel_id}, it might already be unarchived or another error occurred: {e}"
|
||||
)
|
||||
|
||||
|
||||
def _add_slack_conversation_members(
|
||||
slack_client: WebClient, channel: dict[str, Any], member_ids: list[str]
|
||||
) -> None:
|
||||
channel_id = _get_slack_channel_id(channel)
|
||||
for user_id in member_ids:
|
||||
try:
|
||||
slack_client.conversations_invite(channel=channel_id, users=user_id)
|
||||
except Exception as e:
|
||||
if "already_in_channel" in str(e):
|
||||
continue
|
||||
logger.error(f"Error inviting member: {e}")
|
||||
logger.error(user_id)
|
||||
|
||||
|
||||
def _delete_slack_conversation_messages(
|
||||
slack_client: WebClient,
|
||||
channel: dict[str, Any],
|
||||
message_to_delete: str | None = None,
|
||||
) -> None:
|
||||
"""deletes all messages from a channel if message_to_delete is None"""
|
||||
channel_id = _get_slack_channel_id(channel)
|
||||
for message_batch in get_channel_messages(slack_client, channel):
|
||||
for message in message_batch:
|
||||
if default_msg_filter(message):
|
||||
continue
|
||||
|
||||
if message_to_delete and message.get("text") != message_to_delete:
|
||||
continue
|
||||
logger.info(f" removing message: {message.get('text')}")
|
||||
|
||||
try:
|
||||
if not (ts := message.get("ts")):
|
||||
raise ValueError("Message timestamp is missing")
|
||||
slack_client.chat_delete(channel=channel_id, ts=ts)
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting message: {e}")
|
||||
logger.error(message)
|
||||
|
||||
|
||||
def _build_slack_channel_from_name(
|
||||
slack_client: WebClient,
|
||||
admin_user_id: str,
|
||||
suffix: str,
|
||||
is_private: bool,
|
||||
channel: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
base = "public_channel" if not is_private else "private_channel"
|
||||
channel_name = f"{base}-{suffix}"
|
||||
if channel:
|
||||
# If channel is provided, we rename it
|
||||
channel_id = _get_slack_channel_id(channel)
|
||||
channel_response = make_slack_api_call_w_retries(
|
||||
slack_client.conversations_rename,
|
||||
channel=channel_id,
|
||||
name=channel_name,
|
||||
)
|
||||
else:
|
||||
# Otherwise, we create a new channel
|
||||
channel_response = make_slack_api_call_w_retries(
|
||||
slack_client.conversations_create,
|
||||
name=channel_name,
|
||||
is_private=is_private,
|
||||
)
|
||||
|
||||
try:
|
||||
slack_client.conversations_unarchive(channel=channel_response["channel"]["id"])
|
||||
except Exception:
|
||||
# Channel is already unarchived
|
||||
pass
|
||||
try:
|
||||
slack_client.conversations_invite(
|
||||
channel=channel_response["channel"]["id"],
|
||||
users=[admin_user_id],
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
final_channel = channel_response["channel"] if channel_response else {}
|
||||
return final_channel
|
||||
|
||||
|
||||
class SlackManager:
|
||||
@staticmethod
|
||||
def get_slack_client(token: str) -> WebClient:
|
||||
return WebClient(token=token)
|
||||
|
||||
@staticmethod
|
||||
def get_and_provision_available_slack_channels(
|
||||
slack_client: WebClient, admin_user_id: str
|
||||
) -> tuple[dict[str, Any], dict[str, Any], str]:
|
||||
run_id = str(uuid4())
|
||||
public_channels = _get_non_general_channels(
|
||||
slack_client, get_private=False, get_public=True, only_get_done=True
|
||||
)
|
||||
|
||||
first_available_channel = (
|
||||
None if len(public_channels) < 1 else public_channels[0]
|
||||
)
|
||||
public_channel = _build_slack_channel_from_name(
|
||||
slack_client=slack_client,
|
||||
admin_user_id=admin_user_id,
|
||||
suffix=run_id,
|
||||
is_private=False,
|
||||
channel=first_available_channel,
|
||||
)
|
||||
_delete_slack_conversation_messages(
|
||||
slack_client=slack_client, channel=public_channel
|
||||
)
|
||||
|
||||
private_channels = _get_non_general_channels(
|
||||
slack_client, get_private=True, get_public=False, only_get_done=True
|
||||
)
|
||||
second_available_channel = (
|
||||
None if len(private_channels) < 1 else private_channels[0]
|
||||
)
|
||||
private_channel = _build_slack_channel_from_name(
|
||||
slack_client=slack_client,
|
||||
admin_user_id=admin_user_id,
|
||||
suffix=run_id,
|
||||
is_private=True,
|
||||
channel=second_available_channel,
|
||||
)
|
||||
_delete_slack_conversation_messages(
|
||||
slack_client=slack_client, channel=private_channel
|
||||
)
|
||||
|
||||
return public_channel, private_channel, run_id
|
||||
|
||||
@staticmethod
|
||||
def build_slack_user_email_id_map(slack_client: WebClient) -> dict[str, str]:
|
||||
users_results = make_slack_api_call_w_retries(
|
||||
slack_client.users_list,
|
||||
)
|
||||
users: list[dict[str, Any]] = users_results.get("members", [])
|
||||
user_email_id_map = {}
|
||||
for user in users:
|
||||
if not (email := user.get("profile", {}).get("email")):
|
||||
continue
|
||||
if not (user_id := user.get("id")):
|
||||
raise ValueError("User ID is missing")
|
||||
user_email_id_map[email] = user_id
|
||||
return user_email_id_map
|
||||
|
||||
@staticmethod
|
||||
def get_client_user_and_bot_ids(
|
||||
slack_client: WebClient,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Fetches the user ID and bot ID of the authenticated user.
|
||||
"""
|
||||
logger.info("Attempting to find onyxbot user ID and bot ID.")
|
||||
try:
|
||||
auth_response = make_slack_api_call_w_retries(
|
||||
slack_client.auth_test,
|
||||
)
|
||||
id = auth_response.get("user_id")
|
||||
bot_id = auth_response.get("bot_id")
|
||||
return id, bot_id
|
||||
except SlackApiError as e:
|
||||
logger.error(f"Error fetching auth test: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def set_channel_members(
|
||||
slack_client: WebClient,
|
||||
admin_user_id: str,
|
||||
channel: dict[str, Any],
|
||||
user_ids: list[str],
|
||||
) -> None:
|
||||
"""
|
||||
Sets the members of a Slack channel by first removing all members
|
||||
and then adding the specified members.
|
||||
"""
|
||||
_clear_slack_conversation_members(
|
||||
slack_client=slack_client,
|
||||
channel=channel,
|
||||
admin_user_id=admin_user_id,
|
||||
)
|
||||
_add_slack_conversation_members(
|
||||
slack_client=slack_client, channel=channel, member_ids=user_ids
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_message_to_channel(
|
||||
slack_client: WebClient, channel: dict[str, Any], message: str
|
||||
) -> Optional[str]:
|
||||
"""Posts a message to a channel and returns the message timestamp (ts)."""
|
||||
try:
|
||||
channel_id = _get_slack_channel_id(channel)
|
||||
response = make_slack_api_call_w_retries(
|
||||
slack_client.chat_postMessage,
|
||||
channel=channel_id,
|
||||
text=message,
|
||||
)
|
||||
# Return the timestamp of the posted message
|
||||
return response.get("ts")
|
||||
except SlackApiError as e:
|
||||
logger.error(f"Error posting message to channel {channel_id}: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"An unexpected error occurred: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def poll_for_reply(
|
||||
slack_client: WebClient,
|
||||
channel: dict[str, Any],
|
||||
original_message_ts: str,
|
||||
timeout_seconds: int,
|
||||
) -> Optional[dict[str, Any]]:
|
||||
"""
|
||||
Polls a channel for a reply to a specific message for a given duration.
|
||||
"""
|
||||
channel_id = _get_slack_channel_id(channel)
|
||||
start_time = time.time()
|
||||
logger.info(
|
||||
f"Polling channel {channel_id} for reply to message {original_message_ts} for {timeout_seconds} seconds."
|
||||
)
|
||||
|
||||
while time.time() - start_time < timeout_seconds:
|
||||
logger.info(
|
||||
f"""Polling channel {channel_id} | elapsed time: {time.time() - start_time:.2f}
|
||||
seconds out of {timeout_seconds} seconds."""
|
||||
)
|
||||
try:
|
||||
# Fetch recent messages in the thread
|
||||
result = make_slack_api_call_w_retries(
|
||||
slack_client.conversations_replies,
|
||||
channel=channel_id,
|
||||
ts=original_message_ts,
|
||||
limit=20,
|
||||
)
|
||||
messages = result.get("messages", [])
|
||||
|
||||
# The first message is the original message, skip it.
|
||||
for message in messages[1:]:
|
||||
# Check if it's a reply in the correct thread
|
||||
if message.get("thread_ts") == original_message_ts:
|
||||
logger.info(f"Found reply: {message.get('text')}")
|
||||
return message
|
||||
|
||||
except SlackApiError as e:
|
||||
logger.error(f"Error fetching replies from channel {channel_id}: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"An unexpected error occurred during polling: {e}")
|
||||
raise
|
||||
|
||||
# Wait a bit before the next poll
|
||||
time.sleep(1)
|
||||
|
||||
logger.warning(
|
||||
f"Timeout reached. No reply found for message {original_message_ts} in channel {channel_id}."
|
||||
)
|
||||
return None # Timeout reached without finding a reply
|
||||
|
||||
@staticmethod
|
||||
def remove_message_from_channel(
|
||||
slack_client: WebClient, channel: dict[str, Any], message: str
|
||||
) -> None:
|
||||
"""Removes a specific message from the given channel."""
|
||||
_delete_slack_conversation_messages(
|
||||
slack_client=slack_client, channel=channel, message_to_delete=message
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def cleanup_after_test(
|
||||
slack_client: WebClient,
|
||||
test_id: str,
|
||||
) -> None:
|
||||
"""
|
||||
Cleans up Slack channels after a test by renaming channels that contain the test ID.
|
||||
"""
|
||||
channel_types = ["private_channel", "public_channel"]
|
||||
channels: list[dict[str, Any]] = []
|
||||
for result in make_paginated_slack_api_call_w_retries(
|
||||
slack_client.conversations_list,
|
||||
exclude_archived=False,
|
||||
types=channel_types,
|
||||
):
|
||||
channels.extend(result["channels"])
|
||||
|
||||
for channel in channels:
|
||||
if test_id not in channel.get("name", ""):
|
||||
continue
|
||||
# "done" in the channel name indicates that this channel is free to be used for a new test
|
||||
new_name = f"done_{str(uuid4())}"
|
||||
try:
|
||||
slack_client.conversations_rename(channel=channel["id"], name=new_name)
|
||||
except SlackApiError as e:
|
||||
logger.error(f"Error renaming channel {channel['id']}: {e}")
|
||||
|
||||
@staticmethod
|
||||
def get_dm_channel(
|
||||
slack_client: WebClient,
|
||||
user_id: str,
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
response = make_slack_api_call_w_retries(
|
||||
slack_client.conversations_open,
|
||||
users=user_id,
|
||||
)
|
||||
channel = response["channel"]
|
||||
return channel
|
||||
except SlackApiError as e:
|
||||
logger.error(f"Error opening DM channel with user {user_id}: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def delete_all_messages_and_threads(
|
||||
slack_user_client: WebClient,
|
||||
slack_bot_client: WebClient,
|
||||
channel: dict[str, Any],
|
||||
) -> None:
|
||||
"""
|
||||
Deletes all messages and their thread replies in the specified channel.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Deleting all messages and threads in channel {channel}")
|
||||
user_bot_id, _ = SlackManager.get_client_user_and_bot_ids(slack_user_client)
|
||||
# Fetch all messages in the channel
|
||||
channel_id = _get_slack_channel_id(channel)
|
||||
for message_batch in get_channel_messages(slack_user_client, channel):
|
||||
for message in message_batch:
|
||||
user_id = message.get("user")
|
||||
if user_id == "USLACKBOT":
|
||||
continue
|
||||
ts = message.get("ts")
|
||||
if not ts:
|
||||
continue
|
||||
logger.info(f"Deleting message: {message}")
|
||||
# Delete all replies in the thread, if any
|
||||
if message.get("reply_count", 0) > 0:
|
||||
try:
|
||||
replies_result = make_slack_api_call_w_retries(
|
||||
slack_user_client.conversations_replies,
|
||||
channel=channel_id,
|
||||
ts=ts,
|
||||
limit=100,
|
||||
)
|
||||
replies = replies_result.get("messages", [])[
|
||||
1:
|
||||
] # skip parent
|
||||
for reply in replies:
|
||||
logger.info(f"Deleting thread reply: {reply}")
|
||||
user_id = reply.get("user")
|
||||
if user_id == "USLACKBOT":
|
||||
continue
|
||||
client = (
|
||||
slack_user_client
|
||||
if user_id == user_bot_id
|
||||
else slack_bot_client
|
||||
)
|
||||
reply_ts = reply.get("ts")
|
||||
if reply_ts:
|
||||
try:
|
||||
make_slack_api_call_w_retries(
|
||||
client.chat_delete,
|
||||
channel=channel_id,
|
||||
ts=reply_ts,
|
||||
)
|
||||
logger.info("Deleted thread reply")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error deleting thread reply: {e}"
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching thread replies: {e}")
|
||||
|
||||
# Delete the parent/original message
|
||||
try:
|
||||
logger.info(f"Deleting message: {message}")
|
||||
user_id = message.get("user")
|
||||
client = (
|
||||
slack_user_client
|
||||
if user_id == user_bot_id
|
||||
else slack_bot_client
|
||||
)
|
||||
make_slack_api_call_w_retries(
|
||||
client.chat_delete,
|
||||
channel=channel_id,
|
||||
ts=ts,
|
||||
)
|
||||
logger.info(f"Deleted message: {message}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting message: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error deleting all messages and threads in channel {channel_id}: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def get_full_channel_info(
|
||||
slack_client: WebClient,
|
||||
channel: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Fetches full channel information for the specified channel."""
|
||||
logger.info(f"Fetching full channel info for channel {channel}")
|
||||
channel_id = _get_slack_channel_id(channel)
|
||||
try:
|
||||
channel_info = make_slack_api_call_w_retries(
|
||||
slack_client.conversations_info,
|
||||
channel=channel_id,
|
||||
)
|
||||
return channel_info.get("channel", {})
|
||||
except SlackApiError as e:
|
||||
logger.error(f"Error fetching channel info for {channel_id}: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error fetching channel info: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def create_slack_channel(
|
||||
slack_client: WebClient,
|
||||
channel_name: str,
|
||||
is_private: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Creates a new Slack channel (public or private) and returns its details.
|
||||
"""
|
||||
logger.info(f"Creating Slack channel {channel_name} (private: {is_private})")
|
||||
if not channel_name:
|
||||
raise ValueError("Channel name is required")
|
||||
try:
|
||||
# Check if channel already exists
|
||||
for result in make_paginated_slack_api_call_w_retries(
|
||||
slack_client.conversations_list,
|
||||
exclude_archived=False,
|
||||
types=["public_channel", "private_channel"],
|
||||
):
|
||||
for channel in result["channels"]:
|
||||
if channel.get("name") == channel_name:
|
||||
logger.info(
|
||||
f"Channel {channel_name} already exists with ID: {channel['id']}"
|
||||
)
|
||||
return channel
|
||||
|
||||
# Create a new channel if it doesn't exist
|
||||
channel_response = make_slack_api_call_w_retries(
|
||||
slack_client.conversations_create,
|
||||
name=channel_name,
|
||||
is_private=is_private,
|
||||
)
|
||||
channel_id = channel_response["channel"]["id"]
|
||||
logger.info(f"Channel created with ID: {channel_id}")
|
||||
return channel_response["channel"]
|
||||
except SlackApiError as e:
|
||||
logger.error(f"Error creating channel {channel_name}: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error creating channel {channel_name}: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def create_user_group(
|
||||
slack_client: WebClient,
|
||||
group_name: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Checks if a user group exists by name. If it does, returns it.
|
||||
Otherwise, creates a new user group.
|
||||
"""
|
||||
if not group_name:
|
||||
raise ValueError("Group name is required")
|
||||
try:
|
||||
# Check if group already exists
|
||||
response = make_slack_api_call_w_retries(
|
||||
slack_client.usergroups_list,
|
||||
)
|
||||
for group in response.get("usergroups", []):
|
||||
if group.get("name") == group_name:
|
||||
logger.info(
|
||||
f"User group {group_name} already exists with ID: {group['id']}"
|
||||
)
|
||||
return group
|
||||
|
||||
# Create a new user group if not found
|
||||
response = make_slack_api_call_w_retries(
|
||||
slack_client.usergroups_create, name=group_name
|
||||
)
|
||||
usergroup_id = response["usergroup"]["id"]
|
||||
logger.info(f"User group created with ID: {usergroup_id}")
|
||||
return response["usergroup"]
|
||||
except SlackApiError as e:
|
||||
logger.error(f"Error creating or finding user group {group_name}: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in create_user_group for {group_name}: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def set_user_group_members(
|
||||
slack_client: WebClient,
|
||||
user_group_id: str,
|
||||
user_ids: list[str],
|
||||
) -> None:
|
||||
"""
|
||||
Removes all users from the user group and adds the specified users.
|
||||
"""
|
||||
logger.info(f"Setting members of user group {user_group_id} to: {user_ids}")
|
||||
try:
|
||||
# Add specified users
|
||||
if user_ids:
|
||||
make_slack_api_call_w_retries(
|
||||
slack_client.usergroups_users_update,
|
||||
usergroup=user_group_id,
|
||||
users=user_ids,
|
||||
)
|
||||
logger.info(f"Added users to user group {user_group_id}")
|
||||
except SlackApiError as e:
|
||||
logger.error(f"Error resetting members for user group {user_group_id}: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error resetting user group {user_group_id}: {e}")
|
||||
raise
|
||||
@@ -6,6 +6,7 @@ from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import Field
|
||||
from slack_sdk import WebClient
|
||||
|
||||
from onyx.auth.schemas import UserRole
|
||||
from onyx.configs.constants import QAFeedbackType
|
||||
@@ -182,6 +183,22 @@ class DATestSettings(BaseModel):
|
||||
search_time_image_analysis_enabled: bool | None = False
|
||||
|
||||
|
||||
class SlackTestContext(BaseModel):
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
admin_user: DATestUser
|
||||
slack_bot: dict[str, Any]
|
||||
slack_bot_client: WebClient
|
||||
slack_user_client: WebClient
|
||||
slack_secondary_user_client: WebClient
|
||||
std_ans_category: dict[str, Any]
|
||||
std_answer: dict[str, Any]
|
||||
test_channel_1: dict[str, Any]
|
||||
test_channel_2: dict[str, Any]
|
||||
slack_channel_config: dict[str, Any]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DATestIndexAttempt:
|
||||
id: int
|
||||
|
||||
51
backend/tests/integration/common_utils/test_server_utils.py
Normal file
51
backend/tests/integration/common_utils/test_server_utils.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import threading
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from time import sleep
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
|
||||
# FastAPI server for serving files
|
||||
def create_fastapi_app(directory: str) -> FastAPI:
|
||||
"""
|
||||
Creates a FastAPI application that serves static files from a given directory.
|
||||
"""
|
||||
app = FastAPI()
|
||||
|
||||
# Mount the directory to serve static files
|
||||
app.mount("/", StaticFiles(directory=directory, html=True), name="static")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@contextmanager
|
||||
def fastapi_server_context(
|
||||
directory: str, port: int = 8000
|
||||
) -> Generator[None, None, None]:
|
||||
"""
|
||||
Context manager to run a FastAPI server in a separate thread.
|
||||
The server serves static files from the specified directory.
|
||||
"""
|
||||
app = create_fastapi_app(directory)
|
||||
|
||||
config = uvicorn.Config(app=app, host="0.0.0.0", port=port, log_level="info")
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
# Create a thread to run the FastAPI server
|
||||
server_thread = threading.Thread(target=server.run)
|
||||
server_thread.daemon = (
|
||||
True # Ensures the thread will exit when the main program exits
|
||||
)
|
||||
|
||||
try:
|
||||
# Start the server in the background
|
||||
server_thread.start()
|
||||
sleep(5) # Give it a few seconds to start
|
||||
yield # Yield control back to the calling function (context manager in use)
|
||||
finally:
|
||||
# Shutdown the server
|
||||
server.should_exit = True
|
||||
server_thread.join()
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.integration.connector_job_tests.slack.slack_api_utils import SlackManager
|
||||
from tests.integration.common_utils.managers.slack import SlackManager
|
||||
|
||||
# from tests.load_env_vars import load_env_vars
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from tests.integration.common_utils.test_models import DATestConnector
|
||||
from tests.integration.common_utils.test_models import DATestCredential
|
||||
from tests.integration.common_utils.test_models import DATestUser
|
||||
from tests.integration.common_utils.vespa import vespa_fixture
|
||||
from tests.integration.connector_job_tests.slack.slack_api_utils import SlackManager
|
||||
from tests.integration.common_utils.managers.slack import SlackManager
|
||||
|
||||
|
||||
# NOTE(rkuo): it isn't yet clear if the reason these were previously xfail'd
|
||||
|
||||
@@ -21,7 +21,7 @@ from tests.integration.common_utils.test_models import DATestConnector
|
||||
from tests.integration.common_utils.test_models import DATestCredential
|
||||
from tests.integration.common_utils.test_models import DATestUser
|
||||
from tests.integration.common_utils.vespa import vespa_fixture
|
||||
from tests.integration.connector_job_tests.slack.slack_api_utils import SlackManager
|
||||
from tests.integration.common_utils.managers.slack import SlackManager
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="flaky - see DAN-986 for details", strict=False)
|
||||
|
||||
@@ -10,58 +10,18 @@ from datetime import timezone
|
||||
from time import sleep
|
||||
from typing import Any
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from onyx.server.documents.models import DocumentSource
|
||||
from onyx.utils.logger import setup_logger
|
||||
from tests.integration.common_utils.managers.api_key import APIKeyManager
|
||||
from tests.integration.common_utils.managers.cc_pair import CCPairManager
|
||||
from tests.integration.common_utils.managers.user import UserManager
|
||||
from tests.integration.common_utils.test_models import DATestUser
|
||||
from tests.integration.common_utils.test_server_utils import fastapi_server_context
|
||||
from tests.integration.common_utils.vespa import vespa_fixture
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
# FastAPI server for serving files
|
||||
def create_fastapi_app(directory: str) -> FastAPI:
|
||||
app = FastAPI()
|
||||
|
||||
# Mount the directory to serve static files
|
||||
app.mount("/", StaticFiles(directory=directory, html=True), name="static")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# as far as we know, this doesn't hang when crawled. This is good.
|
||||
@contextmanager
|
||||
def fastapi_server_context(
|
||||
directory: str, port: int = 8000
|
||||
) -> Generator[None, None, None]:
|
||||
app = create_fastapi_app(directory)
|
||||
|
||||
config = uvicorn.Config(app=app, host="0.0.0.0", port=port, log_level="info")
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
# Create a thread to run the FastAPI server
|
||||
server_thread = threading.Thread(target=server.run)
|
||||
server_thread.daemon = (
|
||||
True # Ensures the thread will exit when the main program exits
|
||||
)
|
||||
|
||||
try:
|
||||
# Start the server in the background
|
||||
server_thread.start()
|
||||
sleep(5) # Give it a few seconds to start
|
||||
yield # Yield control back to the calling function (context manager in use)
|
||||
finally:
|
||||
# Shutdown the server
|
||||
server.should_exit = True
|
||||
server_thread.join()
|
||||
|
||||
|
||||
# Leaving this here for posterity and experimentation, but the reason we're
|
||||
# not using this is python's web servers hang frequently when crawled
|
||||
# this is obviously not good for a unit test
|
||||
|
||||
341
backend/tests/integration/tests/slack/conftest.py
Normal file
341
backend/tests/integration/tests/slack/conftest.py
Normal file
@@ -0,0 +1,341 @@
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
from time import sleep
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from slack_sdk import WebClient
|
||||
|
||||
from onyx.db.enums import AccessType
|
||||
from onyx.server.documents.models import DocumentSource
|
||||
from onyx.utils.logger import setup_logger
|
||||
from tests.integration.common_utils.managers.cc_pair import CCPairManager
|
||||
from tests.integration.common_utils.managers.file import FileManager
|
||||
from tests.integration.common_utils.managers.llm_provider import LLMProviderManager
|
||||
from tests.integration.common_utils.managers.slack import SlackManager
|
||||
from tests.integration.common_utils.managers.user import UserManager
|
||||
from tests.integration.common_utils.managers.user_group import UserGroupManager
|
||||
from tests.integration.common_utils.reset import reset_all
|
||||
from tests.integration.common_utils.test_models import DATestCCPair
|
||||
from tests.integration.common_utils.test_models import DATestUser
|
||||
from tests.integration.common_utils.test_models import SlackTestContext
|
||||
from tests.integration.common_utils.test_server_utils import fastapi_server_context
|
||||
from tests.integration.tests.slack.utils import create_slack_bot
|
||||
from tests.integration.tests.slack.utils import create_slack_channel_config
|
||||
from tests.integration.tests.slack.utils import create_standard_answer_with_category
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def reset_db_and_index() -> None:
|
||||
"""Fixture to reset the database and Vespa index before each test."""
|
||||
reset_all()
|
||||
logger.info("Database and Vespa index reset successfully.")
|
||||
|
||||
|
||||
def _admin_user() -> DATestUser:
|
||||
"""Creates the admin user once per module."""
|
||||
user = UserManager.create(email="admin@onyx-test.com")
|
||||
logger.info(f"Admin user created: {user.email}")
|
||||
return user
|
||||
|
||||
|
||||
def _slack_bot(admin_user: DATestUser) -> dict[str, Any]:
|
||||
"""Fixture to create a Slack bot and return its details."""
|
||||
|
||||
app_token = os.environ.get("SLACK_APP_TOKEN")
|
||||
bot_token = os.environ.get("SLACK_BOT_TOKEN")
|
||||
if not app_token or not bot_token:
|
||||
raise RuntimeError(
|
||||
"SLACK_APP_TOKEN and/or SLACK_BOT_TOKEN environment variables not set"
|
||||
)
|
||||
|
||||
# Create the Slack bot
|
||||
bot_details = create_slack_bot(
|
||||
bot_token=bot_token, app_token=app_token, user_performing_action=admin_user
|
||||
)
|
||||
logger.info(f"Slack bot created: {bot_details}")
|
||||
return bot_details
|
||||
|
||||
|
||||
def _slack_bot_client() -> WebClient:
|
||||
"""Fixture to create a Slack bot client."""
|
||||
bot_token = os.environ.get("SLACK_BOT_TOKEN")
|
||||
if not bot_token:
|
||||
raise RuntimeError("SLACK_BOT_TOKEN environment variable not set")
|
||||
slack_client = WebClient(token=bot_token)
|
||||
return slack_client
|
||||
|
||||
|
||||
def _slack_user_client() -> WebClient:
|
||||
"""Fixture to create a Slack user client."""
|
||||
user_token = os.environ.get("SLACK_USER_TOKEN")
|
||||
if not user_token:
|
||||
raise RuntimeError("SLACK_USER_TOKEN environment variable not set")
|
||||
slack_client = WebClient(token=user_token)
|
||||
return slack_client
|
||||
|
||||
|
||||
def _slack_secondary_user_client() -> WebClient:
|
||||
"""Fixture to create a secondary Slack user client."""
|
||||
user_token = os.environ.get("SLACK_SECONDARY_USER_TOKEN")
|
||||
if not user_token:
|
||||
raise RuntimeError("SLACK_SECONDARY_USER_TOKEN environment variable not set")
|
||||
slack_client = WebClient(token=user_token)
|
||||
return slack_client
|
||||
|
||||
|
||||
def _create_standard_answer_and_category(
|
||||
admin_user: DATestUser,
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
"""Creates a standard answer category and a standard answer using the combined utility."""
|
||||
category, standard_answer = create_standard_answer_with_category(
|
||||
category_name="IT",
|
||||
keyword="support",
|
||||
answer="If you need any support, Please contact support@onyx.app",
|
||||
user_performing_action=admin_user,
|
||||
)
|
||||
logger.info(f"Standard answer category created: {category}")
|
||||
logger.info(f"Standard answer created: {standard_answer}")
|
||||
return category, standard_answer
|
||||
|
||||
|
||||
def _create_slack_channels(
|
||||
slack_user_client: WebClient,
|
||||
slack_bot_client: WebClient,
|
||||
secondary_user_client: WebClient,
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
"""Creates Slack channels with the bot and user"""
|
||||
slack_bot_user_id, _ = SlackManager.get_client_user_and_bot_ids(slack_bot_client)
|
||||
secondary_user_id, _ = SlackManager.get_client_user_and_bot_ids(
|
||||
secondary_user_client
|
||||
)
|
||||
logger.info(f"Slack bot user ID: {slack_bot_user_id}")
|
||||
logger.info(f"Secondary user ID: {secondary_user_id}")
|
||||
slack_user_id, _ = SlackManager.get_client_user_and_bot_ids(slack_user_client)
|
||||
logger.info(f"Slack user ID: {slack_user_id}")
|
||||
# Create a channels
|
||||
test_channel_1 = SlackManager.create_slack_channel(
|
||||
slack_client=slack_user_client,
|
||||
channel_name="config-test-channel-1",
|
||||
is_private=True,
|
||||
)
|
||||
logger.info(f"Channel created: {test_channel_1}")
|
||||
|
||||
test_channel_2 = SlackManager.create_slack_channel(
|
||||
slack_client=slack_user_client,
|
||||
channel_name="config-test-channel-2",
|
||||
is_private=True,
|
||||
)
|
||||
logger.info(f"Channel created: {test_channel_2}")
|
||||
|
||||
# add the bot to the channels
|
||||
SlackManager.set_channel_members(
|
||||
slack_client=slack_user_client,
|
||||
admin_user_id=slack_user_id,
|
||||
channel=test_channel_1,
|
||||
user_ids=[slack_bot_user_id, secondary_user_id],
|
||||
)
|
||||
logger.info(f"Added bot to channel: {test_channel_1}")
|
||||
SlackManager.set_channel_members(
|
||||
slack_client=slack_user_client,
|
||||
admin_user_id=slack_user_id,
|
||||
channel=test_channel_2,
|
||||
user_ids=[slack_bot_user_id, secondary_user_id],
|
||||
)
|
||||
logger.info(f"Added bot to channel: {test_channel_2}")
|
||||
|
||||
return test_channel_1, test_channel_2
|
||||
|
||||
|
||||
def _create_slack_channel_config(
|
||||
channel_name: str,
|
||||
slack_bot_id: str,
|
||||
admin_user: DATestUser,
|
||||
) -> dict[str, Any]:
|
||||
"""Creates a Slack channel config."""
|
||||
slack_channel_config = create_slack_channel_config(
|
||||
bot_id=slack_bot_id,
|
||||
channel_name=channel_name,
|
||||
user_performing_action=admin_user,
|
||||
)
|
||||
logger.info(f"Slack channel config created: {slack_channel_config}")
|
||||
return slack_channel_config
|
||||
|
||||
|
||||
def _create_file_connector(
|
||||
admin_user: DATestUser, file_name: str, access_type: AccessType = AccessType.PUBLIC
|
||||
) -> DATestCCPair:
|
||||
"""Creates a file connector."""
|
||||
# Upload a file to the connector
|
||||
filepath = f"tests/integration/tests/slack/resources/{file_name}"
|
||||
files = FileManager.upload_file_for_connector(
|
||||
file_path=filepath,
|
||||
file_name=file_name,
|
||||
user_performing_action=admin_user,
|
||||
)
|
||||
logger.info(f"Uploaded {len(files)} documents.")
|
||||
logger.info(f"Files: {files}")
|
||||
file_paths = files["file_paths"]
|
||||
logger.info(f"File paths: {file_paths}")
|
||||
connector_config = {"zip_metadata": {}, "file_locations": file_paths[0:1]}
|
||||
cc_pair = CCPairManager.create_from_scratch(
|
||||
user_performing_action=admin_user,
|
||||
connector_specific_config=connector_config,
|
||||
access_type=access_type,
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
CCPairManager.wait_for_indexing_completion(
|
||||
cc_pair, now, timeout=360, user_performing_action=admin_user
|
||||
)
|
||||
logger.info("Indexing completed successfully.")
|
||||
return cc_pair
|
||||
|
||||
|
||||
def _create_web_connector(admin_user: DATestUser) -> None:
|
||||
"""Creates a web connector."""
|
||||
# Create a web connector
|
||||
test_filename = os.path.realpath(__file__)
|
||||
test_directory = os.path.dirname(test_filename)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
port = 8889
|
||||
website_src = os.path.join(test_directory, "resources")
|
||||
website_tgt = os.path.join(temp_dir, "resources")
|
||||
shutil.copytree(website_src, website_tgt)
|
||||
with fastapi_server_context(os.path.join(temp_dir, "resources"), port):
|
||||
sleep(1) # sleep a tiny bit before starting everything
|
||||
hostname = os.getenv("TEST_WEB_HOSTNAME", "localhost")
|
||||
config = {
|
||||
"base_url": f"http://{hostname}:{port}/index.html",
|
||||
"web_connector_type": "single",
|
||||
}
|
||||
# store the time before we create the connector so that we know after
|
||||
# when the indexing should have started
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# create connector
|
||||
cc_pair_1 = CCPairManager.create_from_scratch(
|
||||
source=DocumentSource.WEB,
|
||||
connector_specific_config=config,
|
||||
user_performing_action=admin_user,
|
||||
)
|
||||
|
||||
CCPairManager.wait_for_indexing_completion(
|
||||
cc_pair_1, now, timeout=60, user_performing_action=admin_user
|
||||
)
|
||||
logger.info("Web connector created successfully.")
|
||||
|
||||
|
||||
def _create_slack_user_group(
|
||||
slack_user_client: WebClient, secondary_user_client: WebClient
|
||||
) -> None:
|
||||
"""Creates a Slack user group."""
|
||||
# Create a user group
|
||||
user_id, _ = SlackManager.get_client_user_and_bot_ids(slack_user_client)
|
||||
secondary_user_id, _ = SlackManager.get_client_user_and_bot_ids(
|
||||
secondary_user_client
|
||||
)
|
||||
logger.info(f"Slack user ID: {user_id}")
|
||||
logger.info(f"Secondary user ID: {secondary_user_id}")
|
||||
user_group = SlackManager.create_user_group(
|
||||
slack_client=slack_user_client,
|
||||
group_name="support",
|
||||
)
|
||||
|
||||
user_group_id = user_group["id"]
|
||||
SlackManager.set_user_group_members(
|
||||
slack_client=slack_user_client,
|
||||
user_group_id=user_group_id,
|
||||
user_ids=[secondary_user_id],
|
||||
)
|
||||
logger.info(f"User group created: {user_group}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def slack_test_context(reset_db_and_index) -> SlackTestContext:
|
||||
"""Fixture to create the Slack test context."""
|
||||
# Create the admin user
|
||||
admin_user = _admin_user()
|
||||
|
||||
# Create a Slack bot
|
||||
slack_bot = _slack_bot(admin_user)
|
||||
|
||||
# Create a Slack bot client
|
||||
slack_bot_client = _slack_bot_client()
|
||||
|
||||
# Create a Slack user client
|
||||
slack_user_client = _slack_user_client()
|
||||
|
||||
# Create a secondary Slack user client
|
||||
slack_secondary_user_client = _slack_secondary_user_client()
|
||||
|
||||
# Create standard answer and category
|
||||
std_ans_category, std_answer = _create_standard_answer_and_category(admin_user)
|
||||
|
||||
# Create Slack channels
|
||||
test_channel_1, test_channel_2 = _create_slack_channels(
|
||||
slack_user_client, slack_bot_client, slack_secondary_user_client
|
||||
)
|
||||
|
||||
# Create Slack channel config
|
||||
slack_channel_config = _create_slack_channel_config(
|
||||
channel_name="config-test-channel-1",
|
||||
slack_bot_id=slack_bot["id"],
|
||||
admin_user=admin_user,
|
||||
)
|
||||
# Return the context
|
||||
return SlackTestContext(
|
||||
admin_user=admin_user,
|
||||
slack_bot=slack_bot,
|
||||
slack_bot_client=slack_bot_client,
|
||||
slack_user_client=slack_user_client,
|
||||
slack_secondary_user_client=slack_secondary_user_client,
|
||||
std_ans_category=std_ans_category,
|
||||
std_answer=std_answer,
|
||||
test_channel_1=test_channel_1,
|
||||
test_channel_2=test_channel_2,
|
||||
slack_channel_config=slack_channel_config,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def setup_module(slack_test_context: SlackTestContext) -> Generator[None, None, None]:
|
||||
|
||||
admin_user = slack_test_context.admin_user
|
||||
|
||||
# Create LLM provider
|
||||
LLMProviderManager.create(user_performing_action=admin_user)
|
||||
|
||||
# Create a file connector
|
||||
_create_file_connector(admin_user, "story.pdf")
|
||||
cc_pair = _create_file_connector(admin_user, "lucky_leaves.pdf", AccessType.PRIVATE)
|
||||
|
||||
# Create a web connector
|
||||
_create_web_connector(admin_user)
|
||||
|
||||
# Create a user group
|
||||
user = UserManager.create(email="subash@onyx.app")
|
||||
UserGroupManager.create(
|
||||
name="onyx-test-user-group",
|
||||
user_ids=[user.id],
|
||||
cc_pair_ids=[cc_pair.id],
|
||||
user_performing_action=admin_user,
|
||||
)
|
||||
|
||||
# Create a Slack user group
|
||||
_create_slack_user_group(
|
||||
slack_user_client=slack_test_context.slack_user_client,
|
||||
secondary_user_client=slack_test_context.slack_secondary_user_client,
|
||||
)
|
||||
|
||||
yield
|
||||
|
||||
# This part will always run after the test, even if it fails
|
||||
# reset_all()
|
||||
logger.info("Test module teardown completed.")
|
||||
18
backend/tests/integration/tests/slack/constants.py
Normal file
18
backend/tests/integration/tests/slack/constants.py
Normal file
@@ -0,0 +1,18 @@
|
||||
QUESTION_LENA_BOOKS = "How many books did Lena receive last Friday?"
|
||||
QUESTION_LENA_BOOKS_NO_MARK = "How many books did Lena receive last Friday"
|
||||
QUESTION_LENA_BOOKS_WEB_SOURCE = (
|
||||
"How many books is mentioned as Lena received in web source?"
|
||||
)
|
||||
QUESTION_NEED_SUPPORT = "I need support"
|
||||
QUESTION_CAPITAL_FRANCE = "What is the capital of France?"
|
||||
QUESTION_HI_GENERIC = "Hi, What are you doing?"
|
||||
ANSWER_LENA_BOOKS_STORY = "42"
|
||||
ANSWER_LENA_BOOKS_WEB = "40"
|
||||
STD_ANSWER_SUPPORT_EMAIL = "support@onyx.app"
|
||||
STD_ANSWER_TEXT = "If you need any support, please contact support@onyx.app"
|
||||
EPHEMERAL_MESSAGE_QUESTION = "How many towering trees are there in the forest?"
|
||||
EPHEMERAL_MESSAGE_ANSWER = "100"
|
||||
DEFAULT_REPLY_TIMEOUT = 180
|
||||
SHORT_REPLY_TIMEOUT = 20
|
||||
SLEEP_BEFORE_EPHEMERAL_CHECK = 30
|
||||
PRIMARY_USER_EMAIL = "subash@onyx.app"
|
||||
67
backend/tests/integration/tests/slack/resources/index.html
Normal file
67
backend/tests/integration/tests/slack/resources/index.html
Normal file
@@ -0,0 +1,67 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lena's Bookstore Story</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Georgia, serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f9f7f4;
|
||||
color: #333;
|
||||
}
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
h1 {
|
||||
color: #5d4037;
|
||||
}
|
||||
.content {
|
||||
background-color: white;
|
||||
padding: 25px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Lena's Bookstore</h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<p>
|
||||
Lena runs a small bookstore in Brooklyn. Every Friday, she receives a
|
||||
new shipment of books.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Last Friday, she received 40 books in total — 14 were mystery novels, 10
|
||||
were science fiction, and the remaining 16 were poetry collections.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
That evening, she sold 23 books: 8 mysteries, 7 sci-fi, and 8 poetry
|
||||
books. Her friend Max, who visited the store at 6:30 PM, noticed that
|
||||
one poetry book was missing from the shelf but wasn't recorded as sold.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
By Saturday morning, Lena did an inventory and found only 18 books left
|
||||
on the shelf, even though she expected 19. She checked the camera
|
||||
footage from 7:15 PM, which revealed that a customer had accidentally
|
||||
placed a poetry book among the used books pile — mystery solved!
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
backend/tests/integration/tests/slack/resources/lucky_leaves.pdf
Normal file
BIN
backend/tests/integration/tests/slack/resources/lucky_leaves.pdf
Normal file
Binary file not shown.
BIN
backend/tests/integration/tests/slack/resources/story.pdf
Normal file
BIN
backend/tests/integration/tests/slack/resources/story.pdf
Normal file
Binary file not shown.
261
backend/tests/integration/tests/slack/slack_test_helpers.py
Normal file
261
backend/tests/integration/tests/slack/slack_test_helpers.py
Normal file
@@ -0,0 +1,261 @@
|
||||
from typing import Any
|
||||
|
||||
from slack_sdk import WebClient
|
||||
|
||||
from onyx.db.chat import get_chat_messages_by_session
|
||||
from onyx.db.models import ChatSession
|
||||
from onyx.db.models import User
|
||||
from onyx.utils.logger import setup_logger
|
||||
from tests.integration.common_utils.managers.slack import SlackManager
|
||||
from tests.integration.common_utils.test_models import DATestUser
|
||||
from tests.integration.tests.slack.constants import PRIMARY_USER_EMAIL
|
||||
from tests.integration.tests.slack.constants import SHORT_REPLY_TIMEOUT
|
||||
from tests.integration.tests.slack.utils import list_slack_channel_configs
|
||||
from tests.integration.tests.slack.utils import update_slack_channel_config
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def send_and_receive_dm(
|
||||
slack_bot_client: WebClient,
|
||||
slack_user_client: WebClient,
|
||||
message: str,
|
||||
timeout_secs: int = 200,
|
||||
) -> Any:
|
||||
"""Sends a direct message (DM) from a user to the Onyx bot and waits for a reply."""
|
||||
user_id, bot_id = SlackManager.get_client_user_and_bot_ids(slack_bot_client)
|
||||
logger.info(f"Slack bot user ID: {user_id}, Slack bot ID: {bot_id}")
|
||||
|
||||
# Open DM channel with the bot
|
||||
channel = SlackManager.get_dm_channel(slack_user_client, user_id)
|
||||
logger.info(f"Opened DM channel {channel} with bot {bot_id}")
|
||||
|
||||
# Post a message as the user
|
||||
# This message is sent to the bot, not the channel
|
||||
original_msg_ts = SlackManager.add_message_to_channel(
|
||||
slack_user_client, channel=channel, message=message
|
||||
)
|
||||
logger.info(f"Posted message to channel {channel}")
|
||||
|
||||
# Wait for the bot to respond
|
||||
reply = SlackManager.poll_for_reply(
|
||||
slack_user_client,
|
||||
channel=channel,
|
||||
original_message_ts=original_msg_ts,
|
||||
timeout_seconds=timeout_secs,
|
||||
)
|
||||
logger.info(f"Received message from bot: {reply}")
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
def send_dm_with_optional_timeout(
|
||||
slack_bot_client: WebClient,
|
||||
slack_user_client: WebClient,
|
||||
message_text: str,
|
||||
expected_text: str = None,
|
||||
):
|
||||
"""Sends a DM and receives a reply, using a shorter timeout if no specific reply text is expected."""
|
||||
if expected_text is None:
|
||||
return send_and_receive_dm(
|
||||
slack_bot_client,
|
||||
slack_user_client,
|
||||
message_text,
|
||||
timeout_secs=SHORT_REPLY_TIMEOUT,
|
||||
)
|
||||
else:
|
||||
return send_and_receive_dm(slack_bot_client, slack_user_client, message_text)
|
||||
|
||||
|
||||
def send_channel_msg_with_optional_timeout(
|
||||
slack_bot_client: WebClient,
|
||||
slack_user_client: WebClient,
|
||||
message_text: str,
|
||||
channel: dict[str, Any],
|
||||
tag_bot: bool = False,
|
||||
expected_text: str = None,
|
||||
):
|
||||
"""Sends a channel message and receives a reply, using a shorter timeout if no specific reply text is expected."""
|
||||
|
||||
if expected_text is None:
|
||||
return send_and_receive_channel_message(
|
||||
slack_user_client=slack_user_client,
|
||||
slack_bot_client=slack_bot_client,
|
||||
message=message_text,
|
||||
channel=channel,
|
||||
tag_bot=tag_bot,
|
||||
timeout_secs=SHORT_REPLY_TIMEOUT,
|
||||
)
|
||||
else:
|
||||
return send_and_receive_channel_message(
|
||||
slack_user_client=slack_user_client,
|
||||
slack_bot_client=slack_bot_client,
|
||||
message=message_text,
|
||||
channel=channel,
|
||||
tag_bot=tag_bot,
|
||||
)
|
||||
|
||||
|
||||
def send_message_to_channel(
|
||||
slack_user_client: WebClient,
|
||||
slack_bot_client: WebClient,
|
||||
message: str,
|
||||
channel: dict[str, Any],
|
||||
tag_bot: bool = False,
|
||||
) -> str:
|
||||
"""Sends a message to a specified Slack channel as the user."""
|
||||
user_id, _ = SlackManager.get_client_user_and_bot_ids(slack_bot_client)
|
||||
logger.info(f"Slack bot user ID: {user_id}")
|
||||
|
||||
# tag the bot in the message if required
|
||||
if tag_bot:
|
||||
message = f"<@{user_id}> {message}"
|
||||
|
||||
logger.info(f"Sending message to channel {channel['name']}: {message}")
|
||||
# Post a message as the user
|
||||
original_msg_ts = SlackManager.add_message_to_channel(
|
||||
slack_user_client, channel=channel, message=message
|
||||
)
|
||||
logger.info(f"Posted message to channel {channel['name']}")
|
||||
return original_msg_ts
|
||||
|
||||
|
||||
def send_and_receive_channel_message(
|
||||
slack_user_client: WebClient,
|
||||
slack_bot_client: WebClient,
|
||||
message: str,
|
||||
channel: dict[str, Any],
|
||||
tag_bot: bool = False,
|
||||
timeout_secs: int = 200,
|
||||
) -> Any:
|
||||
"""Sends a message to a channel and waits for the bot's reply."""
|
||||
original_msg_ts = send_message_to_channel(
|
||||
slack_user_client,
|
||||
slack_bot_client,
|
||||
message=message,
|
||||
channel=channel,
|
||||
tag_bot=tag_bot,
|
||||
)
|
||||
# Wait for the bot to respond
|
||||
reply = SlackManager.poll_for_reply(
|
||||
slack_user_client,
|
||||
channel=channel,
|
||||
original_message_ts=original_msg_ts,
|
||||
timeout_seconds=timeout_secs,
|
||||
)
|
||||
logger.info(f"Received message from bot: {reply}")
|
||||
return reply
|
||||
|
||||
|
||||
def update_channel_config(
|
||||
bot_id: str,
|
||||
user_performing_action: DATestUser,
|
||||
channel_name: str | None = None,
|
||||
updated_config_data: dict[str, Any] = {},
|
||||
) -> dict[str, Any]:
|
||||
"""Updates the configuration for a specific Slack channel or the default channel."""
|
||||
# Get all channel configs
|
||||
channel_configs = list_slack_channel_configs(
|
||||
bot_id=bot_id, user_performing_action=user_performing_action
|
||||
)
|
||||
logger.info(f"Channel configs: {channel_configs}")
|
||||
|
||||
for channel_config in channel_configs:
|
||||
inner_channel_config = channel_config["channel_config"]
|
||||
channel_name_from_config = inner_channel_config["channel_name"]
|
||||
logger.info(f"Channel name: {channel_name_from_config} | {channel_name}")
|
||||
if channel_name is None and channel_config["is_default"]:
|
||||
channel_config_id = channel_config.get("id")
|
||||
channel_name = channel_config.get("channel_name")
|
||||
logger.info(f"Found default channel config ID: {channel_config_id}")
|
||||
break
|
||||
elif inner_channel_config.get("channel_name") == channel_name:
|
||||
channel_config_id = channel_config.get("id")
|
||||
logger.info(f"Found channel config ID: {channel_config_id}")
|
||||
break
|
||||
else:
|
||||
logger.error("No channel config found.")
|
||||
raise ValueError("No channel config found.")
|
||||
|
||||
logger.info(f"Channel config ID: {channel_config_id}")
|
||||
|
||||
channel_config = {
|
||||
"slack_bot_id": bot_id,
|
||||
"channel_name": channel_name or "None",
|
||||
"respond_tag_only": True,
|
||||
"response_type": "citations",
|
||||
}
|
||||
|
||||
channel_config_to_update = {**channel_config, **updated_config_data}
|
||||
logger.info(f"Channel config to update: {channel_config_to_update}")
|
||||
|
||||
# Update the channel config
|
||||
updated_channel_config = update_slack_channel_config(
|
||||
config_id=channel_config_id,
|
||||
user_performing_action=user_performing_action,
|
||||
update_data=channel_config_to_update,
|
||||
)
|
||||
|
||||
logger.info(f"Updated channel config: {updated_channel_config}")
|
||||
return updated_channel_config
|
||||
|
||||
|
||||
def assert_button_presence(
|
||||
blocks: list[dict[str, Any]],
|
||||
action_id: str,
|
||||
should_exist: bool,
|
||||
test_name: str,
|
||||
channel_name: str = "DM",
|
||||
):
|
||||
"""Asserts the presence or absence of a button with a specific action_id within Slack message blocks."""
|
||||
found_button = False
|
||||
for block in blocks:
|
||||
if block["type"] == "actions":
|
||||
for element in block.get("elements", []):
|
||||
if (
|
||||
element.get("type") == "button"
|
||||
and element.get("action_id") == action_id
|
||||
):
|
||||
found_button = True
|
||||
break
|
||||
if found_button:
|
||||
break
|
||||
if should_exist:
|
||||
assert found_button, f"{test_name}: Button should be present in {channel_name}"
|
||||
else:
|
||||
assert (
|
||||
not found_button
|
||||
), f"{test_name}: Button should NOT be present in {channel_name}"
|
||||
|
||||
|
||||
def get_last_chat_session_and_messages(user_id, db_session):
|
||||
"""
|
||||
Queries the last created chat session for the given user and returns its messages.
|
||||
"""
|
||||
# Get all chat sessions for the user, ordered by time_updated descending
|
||||
chat_sessions = (
|
||||
db_session.query(ChatSession)
|
||||
.filter(ChatSession.user_id == user_id)
|
||||
.order_by(ChatSession.time_updated.desc())
|
||||
.all()
|
||||
)
|
||||
logger.info(f"Chat sessions: {chat_sessions}")
|
||||
if not chat_sessions:
|
||||
logger.warning("No chat sessions found for the user.")
|
||||
return None, []
|
||||
|
||||
last_session = chat_sessions[0]
|
||||
messages = get_chat_messages_by_session(
|
||||
chat_session_id=last_session.id,
|
||||
user_id=user_id,
|
||||
db_session=db_session,
|
||||
)
|
||||
logger.info(f"Retrieved messages: {messages}")
|
||||
return last_session, messages
|
||||
|
||||
|
||||
def get_primary_user_record(db_session):
|
||||
"""
|
||||
Returns the first user record where role is 'SLACK_USER'.
|
||||
"""
|
||||
return db_session.query(User).filter(User.email == PRIMARY_USER_EMAIL).first()
|
||||
1013
backend/tests/integration/tests/slack/test_channels.py
Normal file
1013
backend/tests/integration/tests/slack/test_channels.py
Normal file
File diff suppressed because it is too large
Load Diff
546
backend/tests/integration/tests/slack/test_dm.py
Normal file
546
backend/tests/integration/tests/slack/test_dm.py
Normal file
@@ -0,0 +1,546 @@
|
||||
"""
|
||||
Integration tests for Slack Direct Message (DM) functionality.
|
||||
|
||||
This module contains tests that verify the behavior of the Onyx Slack bot
|
||||
when interacting within DMs, covering various configuration options like
|
||||
responding to bots, showing buttons, filtering messages, standard answers,
|
||||
and handling tags under different restriction settings.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from onyx.utils.logger import setup_logger
|
||||
from tests.integration.common_utils.managers.slack import SlackManager
|
||||
from tests.integration.common_utils.test_models import SlackTestContext
|
||||
from tests.integration.tests.slack.constants import ANSWER_LENA_BOOKS_STORY
|
||||
from tests.integration.tests.slack.constants import ANSWER_LENA_BOOKS_WEB
|
||||
from tests.integration.tests.slack.constants import QUESTION_CAPITAL_FRANCE
|
||||
from tests.integration.tests.slack.constants import QUESTION_LENA_BOOKS
|
||||
from tests.integration.tests.slack.constants import QUESTION_LENA_BOOKS_NO_MARK
|
||||
from tests.integration.tests.slack.constants import QUESTION_LENA_BOOKS_WEB_SOURCE
|
||||
from tests.integration.tests.slack.constants import QUESTION_NEED_SUPPORT
|
||||
from tests.integration.tests.slack.constants import SHORT_REPLY_TIMEOUT
|
||||
from tests.integration.tests.slack.constants import STD_ANSWER_SUPPORT_EMAIL
|
||||
from tests.integration.tests.slack.slack_test_helpers import assert_button_presence
|
||||
from tests.integration.tests.slack.slack_test_helpers import send_and_receive_dm
|
||||
from tests.integration.tests.slack.slack_test_helpers import (
|
||||
send_dm_with_optional_timeout,
|
||||
)
|
||||
from tests.integration.tests.slack.slack_test_helpers import update_channel_config
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
# Note: Messages sent by this test suite are treated as bot messages by Slack.
|
||||
# Therefore, the 'respond_to_bots' configuration option needs to be enabled
|
||||
# in most test cases to ensure the bot responds as expected.
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_name, config_update",
|
||||
[
|
||||
("enabled", {"respond_to_bots": False}),
|
||||
("disabled", {"disabled": True}),
|
||||
],
|
||||
)
|
||||
def test_dm_default_config(
|
||||
slack_test_context: SlackTestContext,
|
||||
test_name: str,
|
||||
config_update: dict[str, Any] | None,
|
||||
) -> None:
|
||||
"""Test cases for Slack DMs using the default configuration."""
|
||||
logger.info(f"Testing DM config: {test_name}")
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
admin_user = slack_test_context.admin_user
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
|
||||
if config_update:
|
||||
update_channel_config(bot_id, admin_user, updated_config_data=config_update)
|
||||
|
||||
message = send_and_receive_dm(
|
||||
slack_bot_client,
|
||||
slack_user_client,
|
||||
QUESTION_LENA_BOOKS,
|
||||
timeout_secs=SHORT_REPLY_TIMEOUT,
|
||||
)
|
||||
# Message is treated as bot message so we won't get a response
|
||||
assert message is None, f"Bot should not respond when {test_name}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_name, config_update, expected_text",
|
||||
[
|
||||
(
|
||||
"enabled",
|
||||
{"respond_to_bots": True},
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
("disabled", {"disabled": True, "respond_to_bots": True}, None),
|
||||
],
|
||||
)
|
||||
def test_dm_default_config_by_enabling_respond_to_bot(
|
||||
slack_test_context: SlackTestContext,
|
||||
test_name: str,
|
||||
config_update: dict[str, Any],
|
||||
expected_text: list[str] | None,
|
||||
) -> None:
|
||||
"""
|
||||
Test Slack DMs with the 'respond_to_bots' setting enabled.
|
||||
|
||||
Verifies that the bot responds when tagged, even if the message
|
||||
originates from a bot.
|
||||
"""
|
||||
logger.info(f"Testing DM config by enabling respond to bot: {test_name}")
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
admin_user = slack_test_context.admin_user
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
|
||||
if config_update:
|
||||
update_channel_config(bot_id, admin_user, updated_config_data=config_update)
|
||||
message = send_dm_with_optional_timeout(
|
||||
slack_bot_client,
|
||||
slack_user_client,
|
||||
QUESTION_LENA_BOOKS,
|
||||
expected_text=expected_text,
|
||||
)
|
||||
if expected_text is not None:
|
||||
assert message is not None, f"Bot should respond when {test_name}"
|
||||
blocks = message["blocks"]
|
||||
assert (
|
||||
blocks is not None and len(blocks) > 0
|
||||
), f"Response should have blocks when {test_name}"
|
||||
assert any(
|
||||
text in blocks[0]["text"]["text"] for text in expected_text
|
||||
), f"Response should contain one of '{expected_text}' when {test_name}"
|
||||
else:
|
||||
assert message is None, f"Bot should not respond when {test_name}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_name, config_update, expect_button, expected_text",
|
||||
[
|
||||
(
|
||||
"continue_in_web_ui_button_enabled",
|
||||
{"show_continue_in_web_ui": True, "respond_to_bots": True},
|
||||
True,
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
(
|
||||
"continue_in_web_ui_button_disabled",
|
||||
{"show_continue_in_web_ui": False, "respond_to_bots": True},
|
||||
False,
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_dm_continue_in_web_ui_button(
|
||||
slack_test_context: SlackTestContext,
|
||||
test_name: str,
|
||||
config_update: dict[str, Any],
|
||||
expect_button: bool,
|
||||
expected_text: list[str],
|
||||
):
|
||||
"""Test the presence or absence of the 'Continue in Web UI' button in DM responses based on configuration."""
|
||||
logger.info(f"Testing DM continue_in_web_ui button: {test_name}")
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
admin_user = slack_test_context.admin_user
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
|
||||
update_channel_config(bot_id, admin_user, updated_config_data=config_update)
|
||||
message = send_and_receive_dm(
|
||||
slack_bot_client,
|
||||
slack_user_client,
|
||||
QUESTION_LENA_BOOKS,
|
||||
)
|
||||
assert message is not None, f"{test_name}: Bot should respond"
|
||||
blocks = message["blocks"]
|
||||
assert (
|
||||
blocks is not None and len(blocks) > 0
|
||||
), f"{test_name}: Response should have blocks"
|
||||
assert any(
|
||||
text in blocks[0]["text"]["text"] for text in expected_text
|
||||
), f"{test_name}: Response should contain one of {expected_text}"
|
||||
assert_button_presence(blocks, "continue-in-web-ui", expect_button, test_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_name, config_update, expect_button, expected_text",
|
||||
[
|
||||
(
|
||||
"follow_up_tags_enabled",
|
||||
{"follow_up_tags": ["help@onyx.app"], "respond_to_bots": True},
|
||||
True,
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
(
|
||||
"follow_up_tags_disabled",
|
||||
{"respond_to_bots": True},
|
||||
False,
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_dm_follow_up_button(
|
||||
slack_test_context: SlackTestContext,
|
||||
test_name: str,
|
||||
config_update: dict[str, Any],
|
||||
expect_button: bool,
|
||||
expected_text: list[str],
|
||||
):
|
||||
"""Test the presence or absence of the 'Follow-up' button in DM responses based on configuration."""
|
||||
logger.info(f"Testing DM follow_up button: {test_name}")
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
admin_user = slack_test_context.admin_user
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
|
||||
update_channel_config(bot_id, admin_user, updated_config_data=config_update)
|
||||
message = send_and_receive_dm(
|
||||
slack_bot_client,
|
||||
slack_user_client,
|
||||
QUESTION_LENA_BOOKS,
|
||||
)
|
||||
assert message is not None, f"{test_name}: Bot should respond"
|
||||
blocks = message["blocks"]
|
||||
assert (
|
||||
blocks is not None and len(blocks) > 0
|
||||
), f"{test_name}: Response should have blocks"
|
||||
assert any(
|
||||
text in blocks[0]["text"]["text"] for text in expected_text
|
||||
), f"{test_name}: Response should contain one of {expected_text}"
|
||||
assert_button_presence(blocks, "followup-button", expect_button, test_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_name, config_update, message_text, expected_text",
|
||||
[
|
||||
(
|
||||
"respond_to_questions_enabled_with_question",
|
||||
{"respond_to_bots": True, "answer_filters": ["questionmark_prefilter"]},
|
||||
QUESTION_LENA_BOOKS,
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
(
|
||||
"respond_to_questions_enabled_without_question",
|
||||
{"respond_to_bots": True, "answer_filters": ["questionmark_prefilter"]},
|
||||
QUESTION_LENA_BOOKS_NO_MARK,
|
||||
None,
|
||||
),
|
||||
(
|
||||
"respond_to_questions_enabled_without_question",
|
||||
{"respond_to_bots": True},
|
||||
QUESTION_LENA_BOOKS_NO_MARK,
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_dm_respond_to_questions(
|
||||
slack_test_context: SlackTestContext,
|
||||
test_name: str,
|
||||
config_update: dict[str, Any],
|
||||
message_text: str,
|
||||
expected_text: list[str] | None,
|
||||
):
|
||||
"""Test the 'respond_to_questions' filter behavior in DMs."""
|
||||
logger.info(f"Testing DM respond_to_questions: {test_name}")
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
admin_user = slack_test_context.admin_user
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
|
||||
update_channel_config(bot_id, admin_user, updated_config_data=config_update)
|
||||
message = send_dm_with_optional_timeout(
|
||||
slack_bot_client, slack_user_client, message_text, expected_text=expected_text
|
||||
)
|
||||
if expected_text is None:
|
||||
assert message is None, f"{test_name}: Bot should not respond"
|
||||
else:
|
||||
assert message is not None, f"{test_name}: Bot should respond"
|
||||
blocks = message["blocks"]
|
||||
assert (
|
||||
blocks is not None and len(blocks) > 0
|
||||
), f"{test_name}: Response should have blocks"
|
||||
assert any(
|
||||
text in blocks[0]["text"]["text"] for text in expected_text
|
||||
), f"{test_name}: Response should contain one of '{expected_text}'"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_name, config_update, expected_text",
|
||||
[
|
||||
(
|
||||
"with_standard_answer_category",
|
||||
"std_ans_category", # special marker, handled in test
|
||||
STD_ANSWER_SUPPORT_EMAIL,
|
||||
),
|
||||
(
|
||||
"without_standard_answer_category",
|
||||
{"respond_to_bots": True},
|
||||
None, # Should NOT contain standard answer
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_dm_standard_answer_category(
|
||||
slack_test_context: SlackTestContext,
|
||||
test_name: str,
|
||||
config_update: dict[str, Any] | str,
|
||||
expected_text: Any,
|
||||
):
|
||||
"""Test the inclusion of standard answers based on category configuration in DMs."""
|
||||
logger.info(f"Testing DM standard answer category: {test_name}")
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
admin_user = slack_test_context.admin_user
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
|
||||
if config_update == "std_ans_category":
|
||||
cat_id = slack_test_context.std_ans_category["id"]
|
||||
config = {"respond_to_bots": True, "standard_answer_categories": [cat_id]}
|
||||
else:
|
||||
config = config_update
|
||||
|
||||
update_channel_config(bot_id, admin_user, updated_config_data=config)
|
||||
message = send_and_receive_dm(
|
||||
slack_bot_client, slack_user_client, QUESTION_NEED_SUPPORT
|
||||
)
|
||||
assert message is not None, f"{test_name}: Bot should respond"
|
||||
blocks = message["blocks"]
|
||||
if expected_text is None:
|
||||
assert (
|
||||
STD_ANSWER_SUPPORT_EMAIL not in blocks[0]["text"]["text"]
|
||||
), f"{test_name}: Response should NOT contain the standard answer"
|
||||
else:
|
||||
assert (
|
||||
expected_text in blocks[0]["text"]["text"]
|
||||
), f"{test_name}: Response should contain '{expected_text}'"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_name, config_update, expected_text",
|
||||
[
|
||||
(
|
||||
"respond_tag_only_enabled",
|
||||
{"respond_tag_only": True, "respond_to_bots": True},
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
(
|
||||
"respond_tag_only_disabled",
|
||||
{"respond_tag_only": False, "respond_to_bots": True},
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_dm_respond_tag_only(
|
||||
slack_test_context: SlackTestContext,
|
||||
test_name: str,
|
||||
config_update: dict[str, Any],
|
||||
expected_text: list[str] | None,
|
||||
):
|
||||
"""
|
||||
Test the 'respond_tag_only' setting in DMs.
|
||||
|
||||
Note: In DMs, the bot should reply regardless of this setting,
|
||||
as tagging is implicit in a direct message context.
|
||||
"""
|
||||
logger.info(f"Testing DM respond_tag_only: {test_name}")
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
admin_user = slack_test_context.admin_user
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
|
||||
update_channel_config(bot_id, admin_user, updated_config_data=config_update)
|
||||
message = send_and_receive_dm(
|
||||
slack_bot_client,
|
||||
slack_user_client,
|
||||
QUESTION_LENA_BOOKS,
|
||||
)
|
||||
assert message is not None, f"{test_name}: Bot should respond"
|
||||
blocks = message["blocks"]
|
||||
assert (
|
||||
blocks is not None and len(blocks) > 0
|
||||
), f"{test_name}: Response should have blocks"
|
||||
assert any(
|
||||
text in blocks[0]["text"]["text"] for text in expected_text
|
||||
), f"{test_name}: Response should contain one of '{expected_text}'"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_name, config_update, expected_text",
|
||||
[
|
||||
(
|
||||
"respond_to_bots_enabled",
|
||||
{"respond_to_bots": True},
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
("respond_to_bots_disabled", {"respond_to_bots": False}, None),
|
||||
],
|
||||
)
|
||||
def test_dm_respond_to_bots(
|
||||
slack_test_context: SlackTestContext,
|
||||
test_name: str,
|
||||
config_update: dict[str, Any],
|
||||
expected_text: list[str] | None,
|
||||
):
|
||||
"""
|
||||
Test the 'respond_to_bots' setting in DMs.
|
||||
|
||||
Since test messages are treated as bot messages, this directly tests
|
||||
whether the bot responds based on this configuration.
|
||||
"""
|
||||
logger.info(f"Testing DM respond_to_bots: {test_name}")
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
admin_user = slack_test_context.admin_user
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
|
||||
update_channel_config(bot_id, admin_user, updated_config_data=config_update)
|
||||
message = send_dm_with_optional_timeout(
|
||||
slack_bot_client,
|
||||
slack_user_client,
|
||||
QUESTION_LENA_BOOKS,
|
||||
expected_text,
|
||||
)
|
||||
if expected_text is None:
|
||||
assert message is None, f"{test_name}: Bot should not respond"
|
||||
else:
|
||||
assert message is not None, f"{test_name}: Bot should respond"
|
||||
blocks = message["blocks"]
|
||||
assert (
|
||||
blocks is not None and len(blocks) > 0
|
||||
), f"{test_name}: Response should have blocks"
|
||||
assert any(
|
||||
text in blocks[0]["text"]["text"] for text in expected_text
|
||||
), f"{test_name}: Response should contain one of '{expected_text}'"
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Citations are not supported in DM")
|
||||
@pytest.mark.parametrize(
|
||||
"test_name, config_update, expected_text",
|
||||
[
|
||||
(
|
||||
"citations_enabled",
|
||||
{"respond_to_bots": True, "answer_filters": ["well_answered_postfilter"]},
|
||||
"",
|
||||
),
|
||||
("citations_disabled", {"respond_to_bots": True}, None),
|
||||
],
|
||||
)
|
||||
def test_dm_citations(
|
||||
slack_test_context: SlackTestContext,
|
||||
test_name: str,
|
||||
config_update: dict[str, Any],
|
||||
expected_text: Any,
|
||||
):
|
||||
"""Test citation generation in DMs (currently expected to fail)."""
|
||||
logger.info(f"Testing DM citations: {test_name}")
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
admin_user = slack_test_context.admin_user
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
update_channel_config(bot_id, admin_user, updated_config_data=config_update)
|
||||
message = send_dm_with_optional_timeout(
|
||||
slack_bot_client,
|
||||
slack_user_client,
|
||||
QUESTION_CAPITAL_FRANCE,
|
||||
expected_text=expected_text,
|
||||
)
|
||||
if expected_text is None:
|
||||
assert message is None, f"{test_name}: Bot should not respond"
|
||||
else:
|
||||
assert message is not None, f"{test_name}: Bot should respond"
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="Skipping the test on failure, sometimes we are getting correct response and sometimes not."
|
||||
)
|
||||
def test_dm_llm_auto_filters(
|
||||
slack_test_context: SlackTestContext,
|
||||
):
|
||||
"""Test the behavior of LLM auto filters in DMs."""
|
||||
logger.info("Testing DM llm auto filters")
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
admin_user = slack_test_context.admin_user
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
update_channel_config(
|
||||
bot_id,
|
||||
admin_user,
|
||||
updated_config_data={"enable_auto_filters": True, "respond_to_bots": True},
|
||||
)
|
||||
|
||||
message = send_and_receive_dm(
|
||||
slack_bot_client,
|
||||
slack_user_client,
|
||||
QUESTION_LENA_BOOKS_WEB_SOURCE,
|
||||
)
|
||||
assert message is not None, "Bot should respond"
|
||||
blocks = message["blocks"]
|
||||
assert blocks is not None and len(blocks) > 0, "Response should have blocks"
|
||||
assert (
|
||||
ANSWER_LENA_BOOKS_WEB in blocks[0]["text"]["text"]
|
||||
), "Response should contain '40'"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_name, config_update, expected_text",
|
||||
[
|
||||
(
|
||||
"respond_to_bots_disabled_respond_to_questions_disabled",
|
||||
{"respond_to_bots": False},
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
(
|
||||
"respond_to_bots_disabled_respond_to_questions_enabled",
|
||||
{"respond_to_bots": False, "answer_filters": ["questionmark_prefilter"]},
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
(
|
||||
"default_config_disabled",
|
||||
{"disabled": True},
|
||||
[ANSWER_LENA_BOOKS_STORY, ANSWER_LENA_BOOKS_WEB],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_dm_tag_with_restriction(
|
||||
slack_test_context: SlackTestContext,
|
||||
test_name: str,
|
||||
config_update: dict[str, Any],
|
||||
expected_text: list[str] | None,
|
||||
):
|
||||
"""
|
||||
Verify that the bot responds to a direct tag in DMs even when restrictive settings are enabled.
|
||||
|
||||
Tests scenarios where 'respond_to_bots' is disabled, 'respond_to_questions'
|
||||
is enabled (but the message isn't a question), or the overall configuration is disabled.
|
||||
Tagging the bot should override these restrictions in DMs.
|
||||
"""
|
||||
logger.info(f"Testing DM respond_to_bots with restrictions: {test_name}")
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
admin_user = slack_test_context.admin_user
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
|
||||
update_channel_config(bot_id, admin_user, updated_config_data=config_update)
|
||||
|
||||
user_id, _ = SlackManager.get_client_user_and_bot_ids(
|
||||
slack_test_context.slack_bot_client
|
||||
)
|
||||
logger.info(f"Slack bot user ID: {user_id}")
|
||||
|
||||
# tag the bot in the message
|
||||
message = f"<@{user_id}> {QUESTION_LENA_BOOKS_NO_MARK}"
|
||||
logger.info(f"Sending message to bot: {message}")
|
||||
|
||||
message = send_and_receive_dm(slack_bot_client, slack_user_client, message)
|
||||
assert message is not None, f"{test_name}: Bot should respond"
|
||||
blocks = message["blocks"]
|
||||
assert (
|
||||
blocks is not None and len(blocks) > 0
|
||||
), f"{test_name}: Response should have blocks"
|
||||
assert any(
|
||||
text in blocks[0]["text"]["text"] for text in expected_text
|
||||
), f"{test_name}: Response should contain one of '{expected_text}'"
|
||||
133
backend/tests/integration/tests/slack/test_slackbot.py
Normal file
133
backend/tests/integration/tests/slack/test_slackbot.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import pytest
|
||||
|
||||
from onyx.db.engine import get_session_context_manager
|
||||
from onyx.db.slack_bot import fetch_slack_bot
|
||||
from onyx.utils.logger import setup_logger
|
||||
from tests.integration.common_utils.test_models import SlackTestContext
|
||||
from tests.integration.tests.slack.constants import QUESTION_LENA_BOOKS
|
||||
from tests.integration.tests.slack.constants import SHORT_REPLY_TIMEOUT
|
||||
from tests.integration.tests.slack.slack_test_helpers import (
|
||||
send_and_receive_channel_message,
|
||||
)
|
||||
from tests.integration.tests.slack.slack_test_helpers import send_and_receive_dm
|
||||
from tests.integration.tests.slack.utils import delete_slack_bot
|
||||
from tests.integration.tests.slack.utils import get_slack_bot
|
||||
from tests.integration.tests.slack.utils import update_slack_bot
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def test_disable_slack_bot(slack_test_context: SlackTestContext):
|
||||
"""Test the disabling of a Slack bot."""
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
test_channel_1 = slack_test_context.test_channel_1
|
||||
test_channel_2 = slack_test_context.test_channel_2
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
user_performing_action = slack_test_context.admin_user
|
||||
|
||||
# Disable the bot
|
||||
bot_data = get_slack_bot(
|
||||
bot_id=bot_id,
|
||||
user_performing_action=user_performing_action,
|
||||
)
|
||||
update_body = {
|
||||
"name": bot_data.get("name"),
|
||||
"enabled": False,
|
||||
"bot_token": bot_data.get("bot_token"),
|
||||
"app_token": bot_data.get("app_token"),
|
||||
}
|
||||
update_slack_bot(
|
||||
bot_id=bot_id,
|
||||
user_performing_action=user_performing_action,
|
||||
update_body=update_body,
|
||||
)
|
||||
|
||||
# send a message to the bot in dm and check for a response
|
||||
message = send_and_receive_dm(
|
||||
slack_bot_client,
|
||||
slack_user_client,
|
||||
QUESTION_LENA_BOOKS,
|
||||
timeout_secs=SHORT_REPLY_TIMEOUT,
|
||||
)
|
||||
|
||||
assert message is None, "Expected no response from the disabled bot"
|
||||
|
||||
# send a message to the bot in channel 1 and check for a response
|
||||
message = send_and_receive_channel_message(
|
||||
slack_user_client=slack_user_client,
|
||||
slack_bot_client=slack_bot_client,
|
||||
message=QUESTION_LENA_BOOKS,
|
||||
channel=test_channel_1,
|
||||
timeout_secs=SHORT_REPLY_TIMEOUT,
|
||||
)
|
||||
|
||||
assert message is None, "Expected no response from the disabled bot"
|
||||
|
||||
# send a message to the bot in channel 2 and check for a response
|
||||
message = send_and_receive_channel_message(
|
||||
slack_user_client=slack_user_client,
|
||||
slack_bot_client=slack_bot_client,
|
||||
message=QUESTION_LENA_BOOKS,
|
||||
channel=test_channel_2,
|
||||
timeout_secs=SHORT_REPLY_TIMEOUT,
|
||||
)
|
||||
|
||||
assert message is None, "Expected no response from the disabled bot"
|
||||
|
||||
# Enable the bot
|
||||
update_body["enabled"] = True
|
||||
update_slack_bot(
|
||||
bot_id=bot_id,
|
||||
user_performing_action=user_performing_action,
|
||||
update_body=update_body,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.order(-1)
|
||||
def test_delete_slack_bot(slack_test_context: SlackTestContext):
|
||||
"""Test the deletion of a Slack bot."""
|
||||
slack_bot_client = slack_test_context.slack_bot_client
|
||||
slack_user_client = slack_test_context.slack_user_client
|
||||
test_channel_1 = slack_test_context.test_channel_1
|
||||
test_channel_2 = slack_test_context.test_channel_2
|
||||
bot_id = slack_test_context.slack_bot["id"]
|
||||
user_performing_action = slack_test_context.admin_user
|
||||
|
||||
delete_slack_bot(
|
||||
bot_id=bot_id,
|
||||
user_performing_action=user_performing_action,
|
||||
)
|
||||
|
||||
# Verify that the bot is deleted
|
||||
with get_session_context_manager() as session:
|
||||
with pytest.raises(ValueError):
|
||||
fetch_slack_bot(db_session=session, slack_bot_id=bot_id)
|
||||
|
||||
message = send_and_receive_dm(
|
||||
slack_bot_client,
|
||||
slack_user_client,
|
||||
QUESTION_LENA_BOOKS,
|
||||
timeout_secs=SHORT_REPLY_TIMEOUT,
|
||||
)
|
||||
|
||||
assert message is None, "Expected no response from the deleted bot"
|
||||
|
||||
message = send_and_receive_channel_message(
|
||||
slack_user_client=slack_user_client,
|
||||
slack_bot_client=slack_bot_client,
|
||||
message=QUESTION_LENA_BOOKS,
|
||||
channel=test_channel_1,
|
||||
timeout_secs=SHORT_REPLY_TIMEOUT,
|
||||
)
|
||||
|
||||
assert message is None, "Expected no response from the deleted bot"
|
||||
|
||||
message = send_and_receive_channel_message(
|
||||
slack_user_client=slack_user_client,
|
||||
slack_bot_client=slack_bot_client,
|
||||
message=QUESTION_LENA_BOOKS,
|
||||
channel=test_channel_2,
|
||||
timeout_secs=SHORT_REPLY_TIMEOUT,
|
||||
)
|
||||
assert message is None, "Expected no response from the deleted bot"
|
||||
347
backend/tests/integration/tests/slack/utils.py
Normal file
347
backend/tests/integration/tests/slack/utils.py
Normal file
@@ -0,0 +1,347 @@
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
from onyx.utils.logger import setup_logger
|
||||
from tests.integration.common_utils.constants import API_SERVER_URL
|
||||
from tests.integration.common_utils.test_models import DATestUser
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def _format_request_exception_message(
|
||||
e: requests.exceptions.RequestException, context: str
|
||||
) -> str:
|
||||
"""Formats an error message for a RequestException."""
|
||||
error_msg = f"Error {context}: {e}"
|
||||
if hasattr(e, "response") and e.response is not None:
|
||||
error_msg += f" | Response body: {e.response.text}"
|
||||
return error_msg
|
||||
|
||||
|
||||
def create_slack_bot(
|
||||
bot_token: str,
|
||||
app_token: str,
|
||||
user_performing_action: DATestUser,
|
||||
enabled: bool = True,
|
||||
name: str = "test-slack-bot",
|
||||
) -> dict[str, Any]:
|
||||
"""Create a Slack bot using the provided tokens and user information."""
|
||||
body = {
|
||||
"name": name,
|
||||
"enabled": enabled,
|
||||
"bot_token": bot_token,
|
||||
"app_token": app_token,
|
||||
}
|
||||
try:
|
||||
response = requests.post(
|
||||
url=f"{API_SERVER_URL}/manage/admin/slack-app/bots",
|
||||
headers=user_performing_action.headers,
|
||||
json=body,
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_json = response.json()
|
||||
logger.info(f"Slack bot created successfully: {response_json}")
|
||||
return response_json
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_message = _format_request_exception_message(e, "creating Slack bot")
|
||||
logger.error(error_message)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def list_slack_channel_configs(
|
||||
bot_id: int,
|
||||
user_performing_action: DATestUser,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Lists Slack channel configurations for a given bot ID."""
|
||||
try:
|
||||
response = requests.get(
|
||||
url=f"{API_SERVER_URL}/manage/admin/slack-app/bots/{bot_id}/config",
|
||||
headers=user_performing_action.headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_json = response.json()
|
||||
logger.info(
|
||||
f"Successfully listed Slack channel configs for bot {bot_id}: {response_json}"
|
||||
)
|
||||
return response_json
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_message = _format_request_exception_message(
|
||||
e, f"listing Slack channel configs for bot {bot_id}"
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error listing Slack channel configs: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def update_slack_channel_config(
|
||||
config_id: int,
|
||||
update_data: dict[str, Any],
|
||||
user_performing_action: DATestUser,
|
||||
) -> dict[str, Any]:
|
||||
"""Updates a specific Slack channel configuration."""
|
||||
try:
|
||||
response = requests.patch(
|
||||
url=f"{API_SERVER_URL}/manage/admin/slack-app/channel/{config_id}",
|
||||
headers=user_performing_action.headers,
|
||||
json=update_data,
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_json = response.json()
|
||||
logger.info(
|
||||
f"Successfully updated Slack channel config {config_id}: {response_json}"
|
||||
)
|
||||
return response_json
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_message = _format_request_exception_message(
|
||||
e, f"updating Slack channel config {config_id}"
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error updating Slack channel config: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def list_slack_channels_from_api(
|
||||
bot_id: int,
|
||||
user_performing_action: DATestUser,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Lists Slack channels available to the bot via the Slack API."""
|
||||
try:
|
||||
response = requests.get(
|
||||
url=f"{API_SERVER_URL}/manage/admin/slack-app/bots/{bot_id}/channels",
|
||||
headers=user_performing_action.headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_json = response.json()
|
||||
logger.info(
|
||||
f"Successfully listed Slack channels for bot {bot_id} from API: {response_json}"
|
||||
)
|
||||
return response_json
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_message = _format_request_exception_message(
|
||||
e, f"listing Slack channels from API for bot {bot_id}"
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error listing Slack channels from API: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def create_standard_answer_category(
|
||||
category_name: str,
|
||||
user_performing_action: DATestUser,
|
||||
) -> dict[str, Any]:
|
||||
"""Creates a standard answer category."""
|
||||
body = {"name": category_name}
|
||||
try:
|
||||
response = requests.post(
|
||||
url=f"{API_SERVER_URL}/manage/admin/standard-answer/category",
|
||||
headers=user_performing_action.headers,
|
||||
json=body,
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_json = response.json()
|
||||
logger.info(f"Standard answer category created successfully: {response_json}")
|
||||
return response_json
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_message = _format_request_exception_message(
|
||||
e, f"creating standard answer category '{category_name}'"
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error creating standard answer category: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def create_standard_answer(
|
||||
keyword: str,
|
||||
answer: str,
|
||||
category_ids: list[int],
|
||||
user_performing_action: DATestUser,
|
||||
match_regex: bool = False,
|
||||
match_any_keywords: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Creates a standard answer."""
|
||||
body = {
|
||||
"keyword": keyword,
|
||||
"answer": answer,
|
||||
"categories": category_ids,
|
||||
"match_regex": match_regex,
|
||||
"match_any_keywords": match_any_keywords,
|
||||
}
|
||||
try:
|
||||
response = requests.post(
|
||||
url=f"{API_SERVER_URL}/manage/admin/standard-answer",
|
||||
headers=user_performing_action.headers,
|
||||
json=body,
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_json = response.json()
|
||||
logger.info(f"Standard answer created successfully: {response_json}")
|
||||
return response_json
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_message = _format_request_exception_message(
|
||||
e, f"creating standard answer with keyword '{keyword}'"
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error creating standard answer: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def create_standard_answer_with_category(
|
||||
category_name: str,
|
||||
keyword: str,
|
||||
answer: str,
|
||||
user_performing_action: DATestUser,
|
||||
match_regex: bool = False,
|
||||
match_any_keywords: bool = False,
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
"""Creates a standard answer category and then a standard answer using that category."""
|
||||
# Create the category first
|
||||
category = create_standard_answer_category(
|
||||
category_name=category_name,
|
||||
user_performing_action=user_performing_action,
|
||||
)
|
||||
category_id = category.get("id")
|
||||
if category_id is None:
|
||||
raise ValueError("Failed to get ID from created category")
|
||||
|
||||
# Create the standard answer using the new category ID
|
||||
standard_answer = create_standard_answer(
|
||||
keyword=keyword,
|
||||
answer=answer,
|
||||
category_ids=[category_id],
|
||||
user_performing_action=user_performing_action,
|
||||
match_regex=match_regex,
|
||||
match_any_keywords=match_any_keywords,
|
||||
)
|
||||
|
||||
return category, standard_answer
|
||||
|
||||
|
||||
def create_slack_channel_config(
|
||||
bot_id: int,
|
||||
channel_name: str,
|
||||
user_performing_action: DATestUser,
|
||||
config_data: Optional[dict[str, Any]] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Creates a Slack channel config for a given bot and channel.
|
||||
"""
|
||||
|
||||
body = {
|
||||
"slack_bot_id": bot_id,
|
||||
"channel_name": channel_name,
|
||||
"response_type": "citations",
|
||||
"respond_tag_only": True,
|
||||
}
|
||||
|
||||
if config_data:
|
||||
body.update(config_data)
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
url=f"{API_SERVER_URL}/manage/admin/slack-app/channel",
|
||||
headers=user_performing_action.headers,
|
||||
json=body,
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_json = response.json()
|
||||
logger.info(f"Slack channel config created successfully: {response_json}")
|
||||
return response_json
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_message = _format_request_exception_message(
|
||||
e, f"creating Slack channel config for channel '{channel_name}'"
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error creating Slack channel config: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def delete_slack_bot(
|
||||
bot_id: int,
|
||||
user_performing_action: DATestUser,
|
||||
) -> None:
|
||||
"""Deletes a Slack bot using the API."""
|
||||
try:
|
||||
response = requests.delete(
|
||||
url=f"{API_SERVER_URL}/manage/admin/slack-app/bots/{bot_id}",
|
||||
headers=user_performing_action.headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
logger.info(f"Slack bot {bot_id} deleted successfully.")
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_message = _format_request_exception_message(
|
||||
e, f"deleting Slack bot {bot_id}"
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error deleting Slack bot {bot_id}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def get_slack_bot(
|
||||
bot_id: int,
|
||||
user_performing_action: DATestUser,
|
||||
) -> dict[str, Any]:
|
||||
"""Fetches a Slack bot's details using the API."""
|
||||
try:
|
||||
response = requests.get(
|
||||
url=f"{API_SERVER_URL}/manage/admin/slack-app/bots/{bot_id}",
|
||||
headers=user_performing_action.headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
bot_data = response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_message = _format_request_exception_message(
|
||||
e, f"fetching Slack bot {bot_id} details for disabling"
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error fetching Slack bot {bot_id}: {e}")
|
||||
raise
|
||||
logger.info(f"Fetched Slack bot {bot_id} details: {bot_data}")
|
||||
return bot_data
|
||||
|
||||
|
||||
def update_slack_bot(
|
||||
bot_id: int,
|
||||
user_performing_action: DATestUser,
|
||||
update_body: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Updates a Slack bot via the API."""
|
||||
try:
|
||||
response = requests.patch(
|
||||
url=f"{API_SERVER_URL}/manage/admin/slack-app/bots/{bot_id}",
|
||||
headers=user_performing_action.headers,
|
||||
json=update_body,
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_json = response.json()
|
||||
logger.info(f"Slack bot {bot_id} updated successfully: {response_json}")
|
||||
return response_json
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_message = _format_request_exception_message(
|
||||
e, f"updating Slack bot {bot_id}"
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error updating Slack bot {bot_id}: {e}")
|
||||
raise
|
||||
Reference in New Issue
Block a user