Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,18 @@ curl -X POST http://localhost:8004/services/ai_svc/connection-method \

## 📚 Documentation

Complete documentation is available in our [Wiki](../../wiki):
Complete documentation is available in our [Wiki](wiki/):

### English Documentation
- [Getting Started](../../wiki/GETTING_STARTED)
- [Architecture Guide](../../wiki/ARCHITECTURE)
- [API Reference](../../wiki/API)
- [Development Guide](../../wiki/DEVELOPMENT)
- [Production Deployment](../../wiki/PRODUCTION)
- [Getting Started](wiki/en/Quick-Start.md)
- [Architecture Guide](wiki/en/Architecture.md)
- [API Reference](wiki/en/API.md)
- [Development Guide](wiki/en/Development.md)
- [Production Deployment](wiki/en/Production.md)

### Persian Documentation
- [راهنمای شروع](../../wiki/راهنمای-شروع)
- [معماری سیستم](../../wiki/معماری-سیستم)
- [راهنمای شروع](wiki/fa/راهنمای-شروع.md)
- [معماری سیستم](wiki/fa/معماری-سیستم.md)

## 🚀 Deployment

Expand Down
111 changes: 111 additions & 0 deletions services/ai_svc/tests/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@

import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch, AsyncMock

# Make sure the app can be imported
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

from main import app, usage_storage, AIResponse

client = TestClient(app)

# A mock service token for testing
SERVICE_TOKEN = "test_service_token"
os.environ["SERVICE_TOKEN"] = SERVICE_TOKEN
HEADERS = {"X-Service-Token": SERVICE_TOKEN}

@pytest.fixture(autouse=True)
def clear_storage():
"""Clear in-memory usage storage before each test."""
usage_storage.clear()

@pytest.fixture
def mock_openai():
"""Fixture to mock the call_openai_api function."""
with patch("main.call_openai_api", new_callable=AsyncMock) as mock_api:
yield mock_api

def test_health_check():
response = client.get("/health")
assert response.status_code == 200
assert response.json()["status"] == "healthy"
assert response.json()["service"] == "ai_svc"

def test_call_ai_success(mock_openai):
# Arrange
mock_openai.return_value = {
"result": "Mocked AI response",
"model": "gpt-3.5-turbo",
"tokens_used": 50,
"mock": True
}
request_data = {
"prompt": "Test prompt",
"content": "Test content",
"user_id": 1
}

# Act
response = client.post("/call", json=request_data, headers=HEADERS)

# Assert
assert response.status_code == 200
json_response = response.json()
assert json_response["result"] == "Mocked AI response"
mock_openai.assert_called_once()

def test_call_ai_quota_exceeded(mock_openai):
# Arrange
from main import UsageStats
usage_storage[1] = UsageStats(
user_id=1,
total_requests=10,
total_tokens=1000,
requests_today=10,
tokens_today=1000,
quota_remaining=0
)
request_data = {
"prompt": "Test prompt",
"content": "Test content",
"user_id": 1
}

# Act
response = client.post("/call", json=request_data, headers=HEADERS)

# Assert
assert response.status_code == 429
mock_openai.assert_not_called()

@patch("main.call_ai", new_callable=AsyncMock)
def test_summarize_content(mock_call_ai):
# Arrange
mock_response = AIResponse(
result="Mocked summary",
model_used="gpt-3.5-turbo-mock",
tokens_used=25,
processing_time=0.1,
metadata={"test": True}
)
mock_call_ai.return_value = mock_response

request_data = {
"content": "This is a long text to be summarized.",
"max_length": 50
}

# Act
response = client.post("/summarize", json=request_data, headers=HEADERS)

# Assert
assert response.status_code == 200
assert response.json()['result'] == "Mocked summary"
mock_call_ai.assert_called_once()

def test_invalid_service_token():
response = client.post("/call", json={}, headers={"X-Service-Token": "invalid_token"})
assert response.status_code == 401
91 changes: 91 additions & 0 deletions services/channel_mgr_svc/tests/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@

import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch, AsyncMock

# Make sure the app can be imported
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

from main import app

client = TestClient(app)

# A mock service token for testing
SERVICE_TOKEN = "test_service_token"
os.environ["SERVICE_TOKEN"] = SERVICE_TOKEN
HEADERS = {"X-Service-Token": SERVICE_TOKEN}

@pytest.fixture(autouse=True)
def manage_background_task():
"""Fixture to start and stop the background task for tests."""
with patch("main.feed_monitoring_loop", new_callable=AsyncMock):
yield

@pytest.fixture(autouse=True)
def clear_storage():
"""Clear in-memory storage before each test and after."""
from main import channels_storage, feeds_storage

channels_storage.clear()
feeds_storage.clear()

globals_to_reset = {'channel_id_counter': 1, 'feed_id_counter': 1}
for var, value in globals_to_reset.items():
if hasattr(sys.modules['main'], var):
setattr(sys.modules['main'], var, value)

def test_health_check():
response = client.get("/health")
assert response.status_code == 200
assert response.json()["status"] == "healthy"

@pytest.mark.asyncio
@patch("main.test_rss_feed", new_callable=AsyncMock)
async def test_create_feed_invalid_rss(mock_test_rss_feed):
# Arrange
channel_data = {"telegram_id": 12345, "title": "Test Channel", "owner_id": 1}
response = client.post("/channels", json=channel_data, headers=HEADERS)
assert response.status_code == 200
channel_id = response.json()["id"]

mock_test_rss_feed.return_value = {"valid": False}

# Act
feed_data = {"url": "http://invalid-rss.com", "channel_id": channel_id}
response = client.post("/feeds", json=feed_data, headers=HEADERS)

# Assert
assert response.status_code == 400
assert "Invalid RSS feed" in response.json()["detail"]

def test_create_feed_for_nonexistent_channel():
feed_data = {"url": "http://example.com/rss", "channel_id": 999}
response = client.post("/feeds", json=feed_data, headers=HEADERS)
assert response.status_code == 404

@patch("main.check_feed_for_updates", new_callable=AsyncMock)
def test_check_feed_now_endpoint(mock_check_feed):
# Arrange
channel_data = {"telegram_id": 123, "title": "Test", "owner_id": 1}
channel_res = client.post("/channels", json=channel_data, headers=HEADERS)
assert channel_res.status_code == 200
channel_id = channel_res.json()["id"]

with patch("main.test_rss_feed", new_callable=AsyncMock) as mock_test_feed:
mock_test_feed.return_value = {"valid": True, "title": "Test Feed"}
feed_data = {"url": "http://test.com/rss", "channel_id": channel_id}
feed_res = client.post("/feeds", json=feed_data, headers=HEADERS)
assert feed_res.status_code == 200
feed_id = feed_res.json()["id"]

mock_check_feed.return_value = [{"title": "New Post", "link": "http://test.com/post"}]

# Act
response = client.get(f"/feeds/{feed_id}/check", headers=HEADERS)

# Assert
assert response.status_code == 200
assert response.json()["new_items"] == 1
mock_check_feed.assert_called_once()
6 changes: 1 addition & 5 deletions src/rssbot/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@
from typing import Optional
from pydantic import Field

try:
from pydantic_settings import BaseSettings
except ImportError:
# Fallback for older pydantic versions
from pydantic import BaseSettings
from pydantic_settings import BaseSettings


class Config(BaseSettings):
Expand Down
9 changes: 9 additions & 0 deletions wiki/Home.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

# Welcome to the RssBot Platform Wiki

This is the central hub for all documentation related to the RssBot Platform.

Please select your preferred language to get started:

- [**English Documentation**](en/Home.md)
- [**مستندات فارسی (Persian Documentation)**](fa/Home.md)
51 changes: 20 additions & 31 deletions wiki/_Sidebar.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,45 @@

# 📚 RssBot Platform Wiki

## 🏠 Home
- [**🚀 Main Wiki**](Home)
- [**🇺🇸 English Docs**](en/Home)
- [**🇮🇷 فارسی**](fa/Home)
- [**🚀 Main Wiki**](Home.md)
- [**🇺🇸 English Docs**](en/Home.md)
- [**🇮🇷 فارسی**](fa/Home.md)

---

## 🏁 Getting Started
- [📦 Installation](en/Installation)
- [⚡ Quick Start](en/Quick-Start)
- [⚙️ Configuration](en/Configuration)
- [🤖 First Bot](en/First-Bot)
- [📦 Installation](en/Installation.md)
- [⚡ Quick Start](en/Quick-Start.md)
- [🤖 First Bot](en/First-Bot.md)

## 🏗️ Architecture & Design
- [🏛️ Architecture Overview](en/Architecture)
- [🔍 Service Discovery](en/Service-Discovery)
- [🔀 Connection Methods](en/Connection-Methods)
- [📊 Performance](en/Performance)
- [🏛️ Architecture Overview](en/Architecture.md)
- [🔍 Service Discovery](en/Service-Discovery.md)
- [🔀 Connection Methods](en/Connection-Methods.md)

## 👨‍💻 Development
- [🛠️ Development Setup](en/Development)
- [📚 API Reference](en/API)
- [🧪 Testing Guide](en/Testing)
- [🤝 Contributing](en/Contributing)
- [📚 API Reference](en/API.md)
- [🧪 Testing Guide](en/Testing.md)

## 🚀 Deployment & Ops
- [🏭 Production](en/Production)
- [🐳 Docker Guide](en/Docker)
- [☸️ Kubernetes](en/Kubernetes)
- [📈 Monitoring](en/Monitoring)
- [📋 Deployment Checklist](en/Deployment-Checklist.md)

## 🔒 Security
- [🛡️ Security Policy](en/Security)
- [🔐 Authentication](en/Authentication)
- [🔒 Environment Security](en/Environment-Security)
- [🛡️ Security Policy](en/Security.md)

## 🛠️ Advanced
- [🔧 Custom Services](en/Custom-Services)
- [📋 Migration Guide](en/Migration)
- [🚨 Troubleshooting](en/Troubleshooting)
- [⚡ Performance Tuning](en/Performance-Tuning)
- [🚨 Troubleshooting](en/Troubleshooting.md)

---

## 🇮🇷 مستندات فارسی

### شروع کار
- [راهنمای شروع](fa/راهنمای-شروع)
- [شروع سریع](fa/شروع-سریع)
- [پیکربندی](fa/پیکربندی)
- [راهنمای شروع](fa/راهنمای-شروع.md)
- [شروع سریع](fa/شروع-سریع.md)
- [پیکربندی](fa/پیکربندی.md)

### معماری
- [معماری سیستم](fa/معماری-سیستم)
- [راهنمای مهاجرت](fa/راهنمای-مهاجرت-معماری)
- [معماری سیستم](fa/معماری-سیستم.md)
- [راهنمای مهاجرت معماری](fa/راهنمای-مهاجرت-معماری.md)
53 changes: 53 additions & 0 deletions wiki/en/Architecture.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
# RssBot System Architecture

The RssBot platform is designed based on a Hybrid Microservices architecture, aiming to provide maximum flexibility, stability, and scalability.

## Core Components

The RssBot architecture consists of two main parts:

1. **Core Platform:** The brain of the system, located in the `src/rssbot/` path.
2. **Services:** Independent functional units, each responsible for a specific task, located in the `services/` directory.

---

### 1. Core Platform

The core platform includes the following critical components that manage and coordinate the entire system:

#### **Core Controller**

- **Path:** `src/rssbot/core/controller.py`
- **Responsibility:** This controller is the beating heart of the platform. Its main task is Service Discovery, managing their lifecycle, and deciding how to route requests.
- **Functionality:** On startup, the controller identifies all available services and, based on each service's configuration, decides whether to load it as an **In-Process Router** or communicate with it via a **REST API**.

#### **Cached Registry**

- **Path:** `src/rssbot/discovery/cached_registry.py`
- **Responsibility:** This component caches information about active services, their Health Status, and their Connection Method in Redis.
- **Advantage:** By using Redis, service discovery is performed in under a millisecond, which significantly increases the speed of communication between services.

#### **ServiceProxy**

- **Path:** `src/rssbot/discovery/proxy.py`
- **Responsibility:** This class is an intelligent tool for communication between services. Developers can easily call methods of a target service without worrying about its implementation details.
- **Functionality:** The `ServiceProxy` automatically queries the cached registry and selects the best communication method:
- **Router Mode:** If the target service is loaded as an internal router, its method is called directly without network overhead.
- **REST Mode:** If the service is running independently, the `ServiceProxy` sends an HTTP request to the corresponding endpoint.
- **Hybrid Mode:** A combination of the two modes above, maximizing flexibility.

---

### 2. Services

Each service is an independent FastAPI application that provides a specific functionality. This independence allows teams to develop and deploy their service without affecting other parts of the system.

**Examples of Services:**

- **`channel_mgr_svc`:** Manages channels and RSS feeds.
- **`ai_svc`:** Provides artificial intelligence capabilities like content summarization.
- **`bot_svc`:** Communicates with the Telegram API and sends messages.
- **`user_svc`:** Manages users and subscriptions.

This modular and flexible architecture makes RssBot a powerful platform ready for future developments.
=======
# 🏗️ Architecture Overview

Complete guide to RssBot Platform's revolutionary hybrid microservices architecture.
Expand Down
Loading
Loading