70 Commits
1.0.5 ... 1.0.8

Author SHA1 Message Date
6419a8e1d4 Skip SSL certificate verification in web scraping and improve text sorting logic 2025-02-26 17:41:17 +07:00
295bee8727 2025-02-26 17:09:06 +07:00
9c260fde71 Add blacklist check decorator for command execution 2025-02-26 07:41:08 +07:00
7081b27a93 Defer interaction response in stop command and update permission message handling 2025-02-25 23:18:57 +07:00
e92c37dcaf Implement user task management and add command to stop user processes 2025-02-25 23:06:28 +07:00
bc5b00698b Refactor web scraping logic and enhance code sanitization tests for Python and C++ 2025-02-25 21:39:54 +07:00
cbae78491d Enhance code sanitization feedback for Python and C++; refactor tests to use pytest and improve structure 2025-02-25 18:29:54 +07:00
a80b4c64a4 Add g++ compiler and build-essential to Dockerfile dependencies 2025-02-25 18:15:16 +07:00
49beaca848 Add PDF analysis prompt and increase token limit for API messages 2025-02-25 10:35:42 +07:00
65e3a23df2 Ensure ADMIN_ID comparison is consistent by converting user IDs to strings 2025-02-25 09:55:14 +07:00
5a04e7c7a5 Remove redundant synchronous test cases for search and web commands in test_bot.py 2025-02-25 09:32:10 +07:00
2f199c78c1 Change ADMIN_ID to string type for consistency in bot.py 2025-02-25 09:26:15 +07:00
8542819597 Add PyPDF2 dependency and update README with ADMIN_ID configuration, add pdf function for only whitelisted user, add blacklist (aka bot ban). 2025-02-25 09:21:05 +07:00
f9e3e61310 Update bot.py to use asynchronous calls for get_history and get_user_model functions 2025-02-05 23:27:29 +07:00
bc57638638 Refactor bot.py to improve asynchronous handling and enhance message processing 2025-02-05 22:46:18 +07:00
0343599b29 Update save_user_model and save_history calls to be asynchronous 2025-02-03 11:49:43 +07:00
879d34f5b1 Remove GitHub Container Registry login step from workflow 2025-02-03 11:32:21 +07:00
42e7377665 Update GitHub Container Registry login action to use master branch 2025-02-03 11:18:02 +07:00
da1cfe4cd9 Add GitHub Container Registry login step to workflow 2025-02-03 10:25:38 +07:00
Vu Quoc Anh
5be8b3e43c Update main.yml 2025-02-03 10:02:43 +07:00
4fd27b3197 Refactor bot and test code to use asynchronous methods; update get_history calls and improve test structure 2025-02-03 09:38:05 +07:00
1b9d0043e5 Refactor test cases to use asynchronous methods; update history and user model tests, and improve attachment handling 2025-02-03 09:24:17 +07:00
8c8bcc62d8 Refactor MongoDB interactions to use AsyncIOMotorClient for improved performance; update database functions to be asynchronous and add motor dependency 2025-02-03 09:14:41 +07:00
bce901ed9f Update bot configuration and add support for new model; modify timeout settings and improve help command localization 2025-02-03 08:59:40 +07:00
59634cce13 Add latency check to health endpoint and remove bot prefix command and heartbeat timeout 2025-01-23 12:42:02 +07:00
Vu Quoc Anh
6fa264fe75 Update bot.py 2025-01-11 19:37:09 +07:00
Vu Quoc Anh
dd198ac1df Update test_bot.py 2025-01-11 17:54:51 +07:00
Vu Quoc Anh
31550b2556 Update bot.py 2025-01-11 17:52:38 +07:00
Vu Quoc Anh
71b4b4ac73 Update requirements.txt 2025-01-09 21:50:23 +07:00
Vu Quoc Anh
a38156cd97 Update bot.py 2025-01-09 21:49:49 +07:00
Vu Quoc Anh
b2c71db135 Update test_bot.py 2025-01-08 12:30:27 +07:00
Vu Quoc Anh
24e7e250d9 Merge pull request #8 from Coder-Vippro/cauvang32/add-user-stat-command
Add slash command /user_stat to display user statistics
2025-01-07 23:29:21 +07:00
Vu Quoc Anh
5af19d7a30 Add /user_stat command to fetch and display user statistics
* Handle cases where user model is not found, default to `gpt-4o-mini`
* Handle cases where user history is not found or blank, count tokens as 0
* Add unit tests for `/user_stat` command
* Test default model `gpt-4o-mini` if user model not found
* Test token count as 0 if user history is not found or blank
* Add tests for remaining functions in `bot.py`
2025-01-07 23:05:22 +07:00
Vu Quoc Anh
67e806a901 Add slash command /user_stat to display user statistics
Add a new slash command `/user_stat` to fetch and display user statistics.

* **bot.py**
  - Add a new slash command `/user_stat` to fetch and display the current input token, output token, and model for the user.
  - Retrieve the user's history to calculate the input and output tokens.
  - Fetch the model from the database.
  - Update the `help_command` to include the new `/user_stat` command.

* **README.md**
  - Add documentation for the new `/user_stat` command.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/Coder-Vippro/ChatGPT-Discord-Bot?shareId=XXXX-XXXX-XXXX-XXXX).
2025-01-07 22:32:44 +07:00
Vu Quoc Anh
2ea996c365 Update test_bot.py 2025-01-06 23:19:23 +07:00
Vu Quoc Anh
9f0a256b0c Merge pull request #7 from Coder-Vippro/cauvang32/add-slash-command
Add slash command for remaining chat turns and reset
2025-01-06 23:16:27 +07:00
Vu Quoc Anh
6ebbe6c763 Update pull.yml 2025-01-06 23:13:33 +07:00
Vu Quoc Anh
b8a938be42 Add slash command for remaining chat turns and reset
Add functionality to track and reset chat turns for each user and model, and implement a new slash command to check remaining chat turns.

- Add a new MongoDB collection `chat_turns` to track chat turns for each user and model.
- Implement functions to get, update, and reset remaining chat turns.
- Add a daily reset task to reset chat turns for all users and models.
- Add a new slash command `/remaining_turns` to check the remaining chat turns for each model.
- Update the help command to include the new `/remaining_turns` command.

- Add unit tests for the new MongoDB collection `chat_turns`.
- Add unit tests for the daily reset of chat turns.
- Add unit tests for the new slash command `/remaining_turns`.
- Add unit tests for the rate limits for each model.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/Coder-Vippro/ChatGPT-Discord-Bot?shareId=XXXX-XXXX-XXXX-XXXX).
2025-01-06 21:53:28 +07:00
52afa9d41d Add environment variables for MongoDB URI and set deployment environment in GitHub Actions workflow 2025-01-06 18:51:44 +07:00
b30dce5a09 Remove unused imports from test_bot.py to clean up the code 2025-01-06 18:41:07 +07:00
ee82cfbcd8 Refactor test_bot.py to enhance test coverage and structure; update .gitignore to exclude additional files 2025-01-06 18:39:38 +07:00
Vu Quoc Anh
9fe39f4284 Update test_bot.py 2025-01-05 23:20:14 +07:00
Vu Quoc Anh
4ec47f5b6c Update test_bot.py 2025-01-05 23:18:13 +07:00
Vu Quoc Anh
3bd760c5a9 Update test_bot.py 2025-01-05 23:12:37 +07:00
Vu Quoc Anh
6230e5e008 Update Dockerfile 2024-12-31 15:05:53 +07:00
7c4c58949c ok 2024-12-30 18:31:28 +07:00
36c94d64a6 Improve history trimming logic to remove oldest messages first for better token management 2024-12-30 18:31:06 +07:00
Vu Quoc Anh
6bd29626f9 Update main.yml 2024-12-30 17:48:52 +07:00
Vu Quoc Anh
4d3c4ff562 Update main.yml 2024-12-30 15:50:51 +07:00
Vu Quoc Anh
f1f11e76b4 Update main.yml 2024-12-30 15:44:33 +07:00
Vu Quoc Anh
68a2efd69f Update main.yml 2024-12-30 11:52:34 +07:00
Vu Quoc Anh
590fbec630 Update main.yml 2024-12-30 11:44:26 +07:00
4991f6886d Update message handling to set "role" to "system" only for non-"o1" models 2024-12-28 21:54:40 +07:00
a5d6f1e80d Reset "role" to "system" for messages not designated as "developer" in message handling 2024-12-28 21:41:46 +07:00
f0ad2e061d Enhance image handling in model "o1" by adding 'details' key and improve error message for rate limit 2024-12-28 18:18:39 +07:00
08869978f9 Rename "system" role to "developer" for model "o1" and refine image handling logic 2024-12-28 17:53:48 +07:00
acdbdf28f0 Reduce number of search results returned by Google custom search from 3 to 2 2024-12-28 17:28:33 +07:00
57f9d642bb Reduce search results 2024-12-25 10:34:34 +07:00
51a243f9aa Reduce number of search results returned by Google custom search from 5 to 3 2024-12-10 21:17:06 +07:00
503961ba88 Update search functionality to include scraped content and refine prompts 2024-12-10 20:59:37 +07:00
797c1e9e7c Remove user history entry upon error handling in bot 2024-12-03 15:45:39 +07:00
071e77f2c9 Refactor GitHub Actions workflow to rename test job and enhance deployment steps 2024-12-02 23:27:38 +07:00
1353381686 Fix indentation in GitHub Actions workflow for deploy job 2024-12-02 18:36:01 +07:00
276e3424ee Remove commented-out unit test job from GitHub Actions workflow 2024-12-02 18:31:24 +07:00
558d3c3240 Remove redundant checkout step and clean up whitespace in GitHub Actions workflow 2024-12-02 18:28:13 +07:00
50db24fdbb Refactor GitHub Actions workflow to generate and apply Kubernetes secrets from YAML 2024-12-02 18:24:36 +07:00
3e9adc5909 Refactor GitHub Actions workflow to encode secrets as Base64 and create Kubernetes secrets from files 2024-12-02 18:06:16 +07:00
0e297c15f8 Update Kubernetes manifest path in GitHub Actions workflow 2024-12-02 17:56:30 +07:00
5a560eb8ec Add environment specification for deploy job in GitHub Actions workflow 2024-12-02 17:49:20 +07:00
5e2d399422 Refactor GitHub Actions workflow: rename jobs for clarity and streamline deployment steps 2024-12-02 17:46:20 +07:00
8 changed files with 2050 additions and 349 deletions

View File

@@ -6,9 +6,11 @@ on:
- main
jobs:
# Run unit tests for the project
CI:
tests:
runs-on: ubuntu-latest
env:
MONGODB_URI: ${{ secrets.MONGODB_URI }}
environment: Private Server Deploy
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -17,7 +19,15 @@ jobs:
uses: actions/setup-python@v5.3.0
with:
python-version: '3.12.3'
- name: Cache Python dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
@@ -27,23 +37,23 @@ jobs:
- name: Run unit tests
run: |
python -m pytest tests/
# Run security check
- name: pyupio/safety-action
uses: pyupio/safety-action@v1.0.1
with:
api-key: ${{ secrets.SAFETY_API_KEY }}
# Build and push package to GitHub Container Registry (GHCR)
build-and-push-to-ghcr:
runs-on: ubuntu-latest
environment: Private Server Deploy
needs: CI # This job depends on the CI job
build-and-push:
runs-on: ubuntu-latest
environment: Private Server Deploy
needs: tests
steps:
- name: Check out the repository
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
@@ -51,31 +61,22 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build the Docker image
run: |
IMAGE_NAME=ghcr.io/coder-vippro/chatgpt-discord-bot
IMAGE_TAG=latest
docker build -t $IMAGE_NAME:$IMAGE_TAG .
- name: Push the Docker image
run: |
IMAGE_NAME=ghcr.io/coder-vippro/chatgpt-discord-bot
IMAGE_TAG=latest
docker push $IMAGE_NAME:$IMAGE_TAG
- name: Build and push Docker image with cache
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: "ghcr.io/coder-vippro/chatgpt-discord-bot:latest"
cache-from: type=gha
cache-to: type=gha,mode=max
github-token: ${{ secrets.GITHUB_TOKEN }}
# Deploy from GHCR to the main server
deploy-to-main-server:
deploy:
runs-on: self-hosted
environment: Private Server Deploy # Specify the deployment environment
needs: build-and-push-to-ghcr # This job depends on the GHCR push job
needs: build-and-push # This job depends on the GHCR push job
steps:
# Step 1: Log in to GitHub Container Registry
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Step 2: Stop and remove the previous running container
- name: Remove old running container
@@ -109,4 +110,5 @@ jobs:
-e GOOGLE_CX="${{ secrets.GOOGLE_CX }}" \
-e OPENAI_BASE_URL="${{ secrets.OPENAI_BASE_URL }}" \
-e MONGODB_URI="${{ secrets.MONGODB_URI }}" \
$IMAGE_NAME:$IMAGE_TAG
-e ADMIN_ID="${{ secrets.ADMIN_ID }}" \
$IMAGE_NAME:$IMAGE_TAG

View File

@@ -9,6 +9,9 @@ jobs:
# Run unit tests for the project
test:
runs-on: ubuntu-latest
env:
MONGODB_URI: ${{ secrets.MONGODB_URI }}
environment: Private Server Deploy
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -32,4 +35,4 @@ jobs:
- name: pyupio/safety-action
uses: pyupio/safety-action@v1.0.1
with:
api-key: ${{ secrets.SAFETY_API_KEY }}
api-key: ${{ secrets.SAFETY_API_KEY }}

5
.gitignore vendored
View File

@@ -2,3 +2,8 @@ test.py
.env
chat_history.db
bot_copy.py
__pycache__/bot.cpython-312.pyc
tests/__pycache__/test_bot.cpython-312.pyc
.vscode/settings.json
chatgpt.zip
response.txt

View File

@@ -1,22 +1,33 @@
# Use an official Python runtime as a parent image
FROM python:3.11.10-slim
# Install curl and other dependencies
RUN apt-get update && apt-get install -y curl && apt-get clean
# Set environment variables to reduce Python buffer and logs
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Install curl, g++ compiler, and other dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
g++ \
build-essential \
make \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Set the working directory in the container
WORKDIR /usr/src/discordbot
# Copy the requirements file first to leverage Docker cache
COPY requirements.txt ./
COPY requirements.txt .
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Install Python dependencies
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txt
# Expose port for health check
# Expose port (optional, only if needed for a web server)
EXPOSE 5000
# Add health check
# Add health check (update endpoint if needed)
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD curl --fail http://localhost:5000/health || exit 1
@@ -24,4 +35,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
COPY . .
# Command to run the application
CMD ["python3", "bot.py"]
CMD ["python3", "bot.py"]

View File

@@ -1,4 +1,3 @@
# ChatGPT Discord Bot
![Build and Push](https://github.com/coder-vippro/ChatGPT-Discord-Bot/actions/workflows/main.yml/badge.svg)
@@ -40,8 +39,9 @@ To get started, ensure you have:
RUNWARE_API_KEY=your_runware_api_key
GOOGLE_API_KEY=your_google_api_key
GOOGLE_CX=your_google_cx
OPENAI_BASE_URL=https://models.inference.ai.azure.com or https://api.openai.com/v1/models or any api else you want
OPENAI_BASE_URL=https://models.inference.ai.azure.com or https://api.openai.com/v1/models or any openai compatible api else you want
MONGODB_URI=mongodb://localhost:27017/
ADMIN_ID=your_discord_user_id
```
- Use the following `docker-compose.yml`:
```yaml
@@ -72,8 +72,9 @@ To get started, ensure you have:
RUNWARE_API_KEY=your_runware_api_key
GOOGLE_API_KEY=your_google_api_key
GOOGLE_CX=your_google_cx
OPENAI_BASE_URL=https://models.inference.ai.azure.com or https://api.openai.com/v1/models or any api else you want
OPENAI_BASE_URL=https://models.inference.ai.azure.com or https://api.openai.com/v1/models or any openai compatible api else you want
MONGODB_URI=mongodb://localhost:27017/
ADMIN_ID=your_discord_user_id
```
- Install the dependencies:
```bash
@@ -119,6 +120,7 @@ Once the bot is running, it connects to Discord using credentials from `.env`. C
- **Scrape Web Content**: `/web url: "https://example.com"`
- **Search Google**: `/search prompt: "latest news in Vietnam"`
- **Normal chat**: `Ping the bot with a question or send a dms to the bot to start`
- **User Statistics**: `/user_stat` - Get your current input token, output token, and model.
## CI/CD

1862
bot.py

File diff suppressed because it is too large Load Diff

View File

@@ -7,4 +7,7 @@ runware
Pillow
discord.py
pymongo
flask
flask
tiktoken
motor
PyPDF2

View File

@@ -1,63 +1,368 @@
import sys
import os
import unittest
from unittest.mock import AsyncMock, MagicMock
from bot import bot, search, generate_image, web
import asyncio
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
class TestDiscordBotCommands(unittest.TestCase):
# Add parent directory to path for imports
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import bot
class TestBotUtils(unittest.TestCase):
def test_count_tokens(self):
"""Test token counting function"""
# Basic test
self.assertGreater(bot.count_tokens("Hello world"), 0)
# Empty string should return 0 or small value
self.assertLessEqual(bot.count_tokens(""), 3)
# Longer text should have more tokens
short_text = "Hello"
long_text = "Hello " * 100
self.assertLess(bot.count_tokens(short_text), bot.count_tokens(long_text))
def test_prepare_messages_for_api(self):
"""Test message preparation for API"""
messages = [
{"role": "system", "content": "You are a helpful assistant"},
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there"},
{"role": "user", "content": "Help me with Python"}
]
prepared = bot.prepare_messages_for_api(messages, max_tokens=1000)
self.assertIsInstance(prepared, list)
# Should have role and content
for msg in prepared:
self.assertIn("role", msg)
self.assertIn("content", msg)
class TestCodeSanitization(unittest.TestCase):
def test_python_safe_code(self):
"""Test Python code sanitization with safe code"""
code = """
def factorial(n):
if n == 0:
return 1
return n * factorial(n-1)
print(factorial(5))
"""
is_safe, sanitized = bot.sanitize_code(code, "python")
self.assertTrue(is_safe)
self.assertIn("signal.alarm(10)", sanitized)
def test_python_unsafe_code(self):
"""Test Python code sanitization with unsafe imports"""
code = """
import os
print(os.system('ls'))
"""
is_safe, message = bot.sanitize_code(code, "python")
self.assertFalse(is_safe)
self.assertIn("Forbidden module import", message)
def test_cpp_safe_code(self):
"""Test C++ code sanitization with safe code"""
code = """
#include <iostream>
using namespace std;
int main() {
cout << "Hello World" << endl;
return 0;
}
"""
is_safe, sanitized = bot.sanitize_code(code, "cpp")
self.assertTrue(is_safe)
self.assertIn("userMain(", sanitized)
def test_cpp_unsafe_code(self):
"""Test C++ code sanitization with unsafe includes"""
code = """
#include <iostream>
#include <fstream>
int main() {
ofstream file("test.txt");
file << "Hello World";
return 0;
}
"""
is_safe, message = bot.sanitize_code(code, "cpp")
self.assertFalse(is_safe)
self.assertIn("Forbidden header", message)
def test_python_eval_exec_detection(self):
"""Test detection of eval/exec in Python code"""
code = """
print("Hello")
eval("print('This is dangerous')")
"""
is_safe, message = bot.sanitize_code(code, "python")
self.assertFalse(is_safe)
self.assertIn("Forbidden", message)
def test_python_add_missing_structure(self):
"""Test Python code gets proper safety structure"""
code = "print('Hello world')"
is_safe, sanitized = bot.sanitize_code(code, "python")
self.assertTrue(is_safe)
self.assertIn("try:", sanitized)
self.assertIn("except Exception as e:", sanitized)
self.assertIn("finally:", sanitized)
self.assertIn("signal.alarm(0)", sanitized)
def test_cpp_add_missing_iostream(self):
"""Test C++ code gets iostream added when using cout"""
code = """
int main() {
cout << "Hello" << endl;
return 0;
}
"""
is_safe, sanitized = bot.sanitize_code(code, "cpp")
self.assertTrue(is_safe)
self.assertIn("#include <iostream>", sanitized)
def test_cpp_add_missing_namespace(self):
"""Test C++ code gets namespace std added when needed"""
code = """
#include <iostream>
int main() {
cout << "Hello" << endl;
return 0;
}
"""
is_safe, sanitized = bot.sanitize_code(code, "cpp")
self.assertTrue(is_safe)
self.assertIn("using namespace std;", sanitized)
def test_edge_case_empty_code(self):
"""Test sanitization with empty code"""
code = ""
is_safe, sanitized = bot.sanitize_code(code, "python")
self.assertTrue(is_safe)
self.assertNotEqual(sanitized, "") # Should add safety structure
def test_multiple_forbidden_imports(self):
"""Test code with multiple forbidden imports"""
code = """
import os
import sys
import subprocess
print("This is malicious")
"""
is_safe, message = bot.sanitize_code(code, "python")
self.assertFalse(is_safe)
self.assertIn("Forbidden", message)
@pytest.mark.asyncio
class TestAsyncFunctions:
@pytest.fixture
def mock_channel(self):
mock = AsyncMock()
mock.send = AsyncMock()
return mock
@patch('bot.client.chat.completions.create')
async def test_process_batch(self, mock_create, mock_channel):
"""Test batch processing"""
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "Processed content"
mock_create.return_value = mock_response
result = await bot.process_batch(
model="gpt-4o-mini",
user_prompt="Analyze this",
batch_content="Test content",
current_batch=1,
total_batches=1,
channel=mock_channel()
)
# Should call the API
mock_create.assert_called_once()
self.assertTrue(result)
@patch('asyncio.create_subprocess_exec')
async def test_execute_code(self, mock_subprocess):
"""Test code execution"""
# Configure the mock
mock_proc = AsyncMock()
mock_proc.communicate = AsyncMock(return_value=(b"Hello World", b""))
mock_subprocess.return_value = mock_proc
result = await bot.execute_code("print('Hello World')", "python")
self.assertIn("Hello World", result)
class TestToolFunctions(unittest.TestCase):
@patch('bot.google_custom_search')
def test_tool_function_google_search(self, mock_search):
"""Test Google search tool function"""
mock_search.return_value = {"results": [{"title": "Test", "link": "https://example.com"}]}
result = bot.tool_functions["google_search"]({"query": "test", "num_results": 1})
mock_search.assert_called_once_with("test", 1)
self.assertIsInstance(result, dict)
@patch('bot.scrape_web_content')
def test_tool_function_scrape_webpage(self, mock_scrape):
"""Test web scraping tool function"""
mock_scrape.return_value = "Scraped content"
result = bot.tool_functions["scrape_webpage"]({"url": "https://example.com"})
mock_scrape.assert_called_once_with("https://example.com")
self.assertEqual(result, "Scraped content")
@patch('bot.requests.get')
def test_scrape_web_content_detailed(self, mock_requests_get):
"""Test web scraping function with different scenarios"""
# Setup mock response for successful HTML page with article content
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.headers = {'content-type': 'text/html'}
mock_response.content = """
<html><body>
<article>
<h1>Test Article</h1>
<p>This is a test paragraph with meaningful content.</p>
</article>
</body></html>
"""
mock_requests_get.return_value = mock_response
# Test successful scraping with article tag
content = bot.scrape_web_content("https://example.com")
self.assertIn("Test Article", content)
self.assertIn("test paragraph", content)
# Test with HTTP error
mock_response.status_code = 404
mock_response.headers = {'content-type': 'text/html'}
content = bot.scrape_web_content("https://example.com/not-found")
self.assertIn("Error: Received status code 404", content)
# Test with request exception
mock_requests_get.side_effect = Exception("Connection error")
content = bot.scrape_web_content("https://example.com")
self.assertIn("An error occurred", content)
@pytest.mark.asyncio
class TestErrorHandling:
@pytest.fixture
def mock_channel(self):
mock = AsyncMock()
mock.send = AsyncMock()
return mock
@patch('bot.client.chat.completions.create')
async def test_handle_api_error(self, mock_create, mock_channel):
"""Test handling of API errors"""
mock_create.side_effect = Exception("API Error")
channel = mock_channel()
# Call function that would use the API
await bot.send_chatgpt_response(
prompt="Test prompt",
channel=channel,
conversation_history=[],
user_id="12345"
)
# Verify error was handled and communicated
channel.send.assert_called_once()
args, _ = channel.send.call_args
self.assertIn("error", args[0].lower())
@patch('bot.execute_code')
async def test_code_execution_timeout(self, mock_execute):
"""Test handling of code execution timeout"""
mock_execute.side_effect = asyncio.TimeoutError()
result = await bot.process_tool_calls(
{"tool_calls": [{"function": {"name": "code_interpreter", "arguments": '{"code": "print(\\"test\\")", "language": "python"}'}}]},
[]
)
self.assertIn("execution timed out", result[0]["content"].lower())
class TestCommandHandling(unittest.TestCase):
def setUp(self):
self.bot = bot
self.interaction = AsyncMock()
self.interaction.user.id = 123456789 # Mock user ID
self.ctx = MagicMock()
self.ctx.author = MagicMock()
self.ctx.author.id = "12345"
self.ctx.send = AsyncMock()
@patch('bot.prepare_messages_for_api')
@patch('bot.client.chat.completions.create')
async def test_chat_command(self, mock_create, mock_prepare):
"""Test chat command"""
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "Hello, I'm ChatGPT"
mock_create.return_value = mock_response
mock_prepare.return_value = []
# Call chat command
await bot.chat(self.ctx, "Tell me about Python")
# Verify
self.ctx.send.assert_called()
mock_create.assert_called_once()
async def test_search_command(self):
# Set up mocks for interaction methods
self.interaction.response.defer = AsyncMock()
self.interaction.followup.send = AsyncMock()
# Call the search command with a sample query
await search(self.interaction, query="Python")
# Check if followup.send was called
self.interaction.followup.send.assert_called()
self.interaction.response.defer.assert_called_with(thinking=True)
async def test_generate_image_command(self):
# Mock the deferred response
self.interaction.response.defer = AsyncMock()
self.interaction.followup.send = AsyncMock()
# Patch Runware API to return a mock image URL
with unittest.mock.patch('bot.runware.imageInference', return_value=[MagicMock(imageURL="http://example.com/image.png")]):
await generate_image(self.interaction, prompt="Sunset over mountains")
# Check if defer and followup were called
self.interaction.response.defer.assert_called_with(thinking=True)
self.interaction.followup.send.assert_called()
async def test_web_scraping_command(self):
# Mock the interaction methods
self.interaction.response.defer = AsyncMock()
self.interaction.followup.send = AsyncMock()
# Call the web command with a mock URL
await web(self.interaction, url="https://vnexpress.net/nguon-con-khien-arm-huy-giay-phep-chip-voi-qualcomm-4807985.html")
# Ensure a followup message was sent
self.interaction.followup.send.assert_called()
self.interaction.response.defer.assert_called_with(thinking=True)
async def test_message_processing(self):
# Mock a direct message
message = MagicMock()
message.author.id = 987654321
message.content = "Hello, bot!"
message.guild = None # Simulate a DM
# Mock channel.send to test if the bot sends a message
message.channel.send = AsyncMock()
# Test the bot's response
await bot.on_message(message)
message.channel.send.assert_called() # Check if the bot replied
@patch('bot.HISTORY_DB', {}) # Mocking the database dictionary
class TestDatabaseFunctions(unittest.TestCase):
async def test_history_functions(self):
"""Test user history saving and retrieval"""
user_id = "12345"
test_history = [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"}
]
# Test saving history
await bot.save_history(user_id, test_history)
# Test retrieving history
retrieved = await bot.get_history(user_id)
self.assertEqual(retrieved, test_history)
# Test history for new user
new_user = "67890"
new_history = await bot.get_history(new_user)
self.assertEqual(len(new_history), 1) # Should have system message
self.assertEqual(new_history[0]["role"], "system")
@patch('bot.DEFAULT_MODEL', 'gpt-4o-mini')
async def test_user_settings(self):
"""Test user settings management"""
user_id = "12345"
# Test default settings
model = await bot.get_user_model(user_id)
self.assertEqual(model, "gpt-4o-mini")
# Test updating settings
await bot.set_user_model(user_id, "gpt-4o")
updated_model = await bot.get_user_model(user_id)
self.assertEqual(updated_model, "gpt-4o")
# Test invalid model handling
with self.assertRaises(ValueError):
await bot.set_user_model(user_id, "invalid-model")
if __name__ == '__main__':
unittest.main()