Compare commits

...

4 Commits

20 changed files with 3438 additions and 50 deletions

View File

@@ -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"],

View File

@@ -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}"
)

View File

@@ -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

View 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

View File

@@ -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

View 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()

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View 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.")

View 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"

View 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>

View 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()

File diff suppressed because it is too large Load Diff

View 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}'"

View 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"

View 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