Compare commits

..

No commits in common. "74cdc196c64f34b54d7bf72429c975b53d3650f5" and "dbbd366953f8ef250a8db478d5e9046b283de4ac" have entirely different histories.

27 changed files with 382 additions and 1555 deletions

View File

@ -15,14 +15,7 @@
"Bash(.venv/Scripts/python.exe -m pytest tests/ -v --tb=line)",
"Bash(.venv/Scripts/python.exe -m pytest:*)",
"Bash(.venvScriptspython.exe -m pytest tests/unit/ -v --cov=app --cov-report=term-missing)",
"Bash(.\\.venv\\Scripts\\python.exe -m pytest:*)",
"Bash(.\\\\.venv\\\\Scripts\\\\python.exe -m pytest:*)",
"Bash(.venvScriptspython.exe -m pytest tests/unit/test_query.py::TestBenchQueryEndpoint::test_bench_query_success -v)",
"Bash(.venvScriptspython.exe -m pytest tests/unit/ -v --tb=short)",
"Bash(.\\\\\\\\.venv\\\\\\\\Scripts\\\\\\\\python.exe -m pytest:*)",
"Bash(..venvScriptsactivate)",
"Bash(pytest:*)",
"Bash(source:*)"
"Bash(.\\.venv\\Scripts\\python.exe -m pytest:*)"
],
"deny": [],
"ask": []

View File

@ -1,6 +1,6 @@
# Developer Guide
# CLAUDE.md
This file provides detailed technical documentation for developers working with this codebase.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
@ -293,6 +293,6 @@ See [TESTING.md](TESTING.md) for comprehensive testing guide including:
See [PROJECT_STATUS.md](PROJECT_STATUS.md) for detailed implementation status and TODOs. Key points:
- Core infrastructure is complete (auth, DB API client, RAG service)
- All main API endpoints are implemented
- TgBackendInterface is fully implemented
- 99% test coverage (unit + integration + E2E tests)
- Frontend integration complete
- TgBackendInterface is fully implemented (not a stub)
- Frontend integration pending (static/ directory is empty)
- No tests yet (tests/ directory is empty)

View File

@ -1,710 +0,0 @@
```markdown
# SBS Bench RAG API Contract
API для управления пользователями, настройками окружений и сессиями анализа в системе Brief Bench.
## Содержание
- [Общая информация](#общая-информация)
- [Аутентификация и пользователи](#аутентификация-и-пользователи)
- [Настройки пользователя](#настройки-пользователя)
- [Сессии анализа](#сессии-анализа)
- [Форматы данных](#форматы-данных)
- [Обработка ошибок](#обработка-ошибок)
---
## Общая информация
### Base URL
```
/api/v1
```
### Форматы данных
- **Content-Type**: `application/json; charset=utf-8`
- **Даты**: ISO 8601 с таймзоной UTC (`YYYY-MM-DDTHH:MM:SSZ`)
- **UUID**: строка в формате `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
### Окружения
API поддерживает три окружения (enum `environment`):
- `ift` — IFT окружение
- `psi` — PSI окружение
- `prod` — Production окружение
### Режимы API
Два режима работы (enum `api_mode`):
- `bench` — режим тестирования
- `backend` — backend режим
---
## Аутентификация и пользователи
### POST /users/login
Авторизация пользователя и запись информации о логине.
#### Request
```
{
"login": "12345678",
"client_ip": "MTkyLjE2OC4xLjEwMA=="
}
```
**Параметры:**
- `login` (string, required): 8-значный логин (строка из цифр)
- `client_ip` (string, required): IP-адрес в кодировке base64
#### Response 200
```
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"login": "12345678",
"last_login_at": "2025-12-24T12:00:00Z",
"created_at": "2025-12-01T10:00:00Z"
}
```
#### Errors
- **400 Bad Request**: Неверный формат логина или client_ip
- **500 Internal Server Error**: Ошибка сервера
---
## Настройки пользователя
### GET /users/{user_id}/settings
Получить настройки пользователя для всех окружений.
#### Path Parameters
- `user_id` (UUID, required): UUID пользователя
#### Response 200
```
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"settings": {
"ift": {
"apiMode": "bench",
"bearerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"systemPlatform": "platform-ift",
"systemPlatformUser": "user-123",
"platformUserId": "p-user-456",
"platformId": "platform-789",
"withClassify": false,
"resetSessionMode": true
},
"psi": {
"apiMode": "bench",
"bearerToken": null,
"systemPlatform": null,
"systemPlatformUser": null,
"platformUserId": null,
"platformId": null,
"withClassify": false,
"resetSessionMode": true
},
"prod": {
"apiMode": "backend",
"bearerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"systemPlatform": "platform-prod",
"systemPlatformUser": "user-prod",
"platformUserId": "p-user-prod",
"platformId": "platform-prod-id",
"withClassify": true,
"resetSessionMode": false
}
},
"updated_at": "2025-12-24T12:30:00Z"
}
```
#### Errors
- **404 Not Found**: Пользователь не найден
- **500 Internal Server Error**: Ошибка сервера
---
### PATCH /users/{user_id}/settings
Частично обновить настройки пользователя. Обновляются только переданные поля.
#### Path Parameters
- `user_id` (UUID, required): UUID пользователя
#### Request
```
{
"settings": {
"ift": {
"bearerToken": "new-token-value",
"withClassify": true
},
"prod": {
"apiMode": "bench"
}
}
}
```
**Примечания:**
- Передавайте только те окружения и поля, которые нужно изменить
- Непереданные поля остаются без изменений
- Для сброса значения в `null` явно передайте `null`
#### Response 200
```
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"settings": {
"ift": {
"apiMode": "bench",
"bearerToken": "new-token-value",
"systemPlatform": "platform-ift",
"systemPlatformUser": "user-123",
"platformUserId": "p-user-456",
"platformId": "platform-789",
"withClassify": true,
"resetSessionMode": true
},
"psi": {
"apiMode": "bench",
"bearerToken": null,
"systemPlatform": null,
"systemPlatformUser": null,
"platformUserId": null,
"platformId": null,
"withClassify": false,
"resetSessionMode": true
},
"prod": {
"apiMode": "bench",
"bearerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"systemPlatform": "platform-prod",
"systemPlatformUser": "user-prod",
"platformUserId": "p-user-prod",
"platformId": "platform-prod-id",
"withClassify": true,
"resetSessionMode": false
}
},
"updated_at": "2025-12-24T13:00:00Z"
}
```
#### Errors
- **400 Bad Request**: Неверный формат настроек
- **404 Not Found**: Пользователь не найден
- **500 Internal Server Error**: Ошибка сервера
---
## Сессии анализа
### POST /users/{user_id}/sessions
Создать новую сессию анализа.
#### Path Parameters
- `user_id` (UUID, required): UUID пользователя
#### Request
```
{
"environment": "ift",
"api_mode": "bench",
"request": [
{
"body": "Как получить кредит на недвижимость?",
"with_docs": true
},
{
"body": "Какие документы нужны?",
"with_docs": true
}
],
"response": {
"answers": [
{
"question": "Как получить кредит на недвижимость?",
"answer": "Для получения кредита...",
"sources": ["doc1.pdf", "doc2.pdf"]
}
],
"metadata": {
"processing_time_ms": 1250
}
},
"annotations": {
"0": {
"overall": {
"rating": "correct",
"comment": "Ответ корректный и полный"
},
"body_research": {
"issues": [],
"comment": ""
}
}
}
}
```
**Параметры:**
- `environment` (string, required): Окружение (`ift`, `psi`, `prod`)
- `api_mode` (string, required): Режим API (`bench`, `backend`)
- `request` (array, required): Массив запросов
- `body` (string, required): Текст вопроса
- `with_docs` (boolean, optional): Использовать документы (default: `true`)
- `response` (object, required): Ответ системы (произвольная структура)
- `annotations` (object, optional): Аннотации по индексам вопросов
- Ключи должны быть числовыми строками (`"0"`, `"1"`, ...)
#### Response 201
```
{
"session_id": "660e8400-e29b-41d4-a716-446655440000",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"environment": "ift",
"api_mode": "bench",
"request": [
{
"body": "Как получить кредит на недвижимость?",
"with_docs": true
},
{
"body": "Какие документы нужны?",
"with_docs": true
}
],
"response": {
"answers": [
{
"question": "Как получить кредит на недвижимость?",
"answer": "Для получения кредита...",
"sources": ["doc1.pdf", "doc2.pdf"]
}
],
"metadata": {
"processing_time_ms": 1250
}
},
"annotations": {
"0": {
"overall": {
"rating": "correct",
"comment": "Ответ корректный и полный"
},
"body_research": {
"issues": [],
"comment": ""
}
}
},
"created_at": "2025-12-24T14:00:00Z",
"updated_at": "2025-12-24T14:00:00Z"
}
```
#### Errors
- **400 Bad Request**: Неверный формат данных
- **404 Not Found**: Пользователь не найден
- **500 Internal Server Error**: Ошибка сервера
---
### GET /users/{user_id}/sessions
Получить список сессий пользователя с пагинацией.
#### Path Parameters
- `user_id` (UUID, required): UUID пользователя
#### Query Parameters
- `environment` (string, optional): Фильтр по окружению (`ift`, `psi`, `prod`)
- `limit` (integer, optional): Лимит результатов (1-200, default: 50)
- `offset` (integer, optional): Смещение для пагинации (default: 0)
#### Response 200
```
{
"sessions": [
{
"session_id": "660e8400-e29b-41d4-a716-446655440000",
"environment": "ift",
"created_at": "2025-12-24T14:00:00Z"
},
{
"session_id": "770e8400-e29b-41d4-a716-446655440000",
"environment": "ift",
"created_at": "2025-12-23T10:30:00Z"
}
],
"total": 123
}
```
**Сортировка:** По дате создания (от новых к старым)
#### Errors
- **404 Not Found**: Пользователь не найден
- **500 Internal Server Error**: Ошибка сервера
---
### GET /users/{user_id}/sessions/{session_id}
Получить детальную информацию о сессии.
#### Path Parameters
- `user_id` (UUID, required): UUID пользователя
- `session_id` (UUID, required): UUID сессии
#### Response 200
```
{
"session_id": "660e8400-e29b-41d4-a716-446655440000",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"environment": "ift",
"api_mode": "bench",
"request": [
{
"body": "Как получить кредит на недвижимость?",
"with_docs": true
}
],
"response": {
"answers": [
{
"question": "Как получить кредит на недвижимость?",
"answer": "Для получения кредита...",
"sources": ["doc1.pdf"]
}
]
},
"annotations": {
"0": {
"overall": {
"rating": "correct",
"comment": "Ответ корректный"
}
}
},
"created_at": "2025-12-24T14:00:00Z",
"updated_at": "2025-12-24T14:00:00Z"
}
```
#### Errors
- **404 Not Found**: Сессия или пользователь не найдены
- **500 Internal Server Error**: Ошибка сервера
---
### PATCH /users/{user_id}/sessions/{session_id}
Обновить аннотации сессии (например, после ревью).
#### Path Parameters
- `user_id` (UUID, required): UUID пользователя
- `session_id` (UUID, required): UUID сессии
#### Request
```
{
"annotations": {
"0": {
"overall": {
"rating": "incorrect",
"comment": "Ответ неполный, не хватает информации о процентах"
},
"body_research": {
"issues": ["missing_info"],
"comment": "Не указаны процентные ставки"
}
},
"1": {
"overall": {
"rating": "correct",
"comment": "Ответ корректный"
}
}
}
}
```
**Примечания:**
- Ключи `annotations` должны быть числовыми строками (`"0"`, `"1"`, ...)
- Полностью заменяет существующие аннотации
#### Response 200
```
{
"session_id": "660e8400-e29b-41d4-a716-446655440000",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"environment": "ift",
"api_mode": "bench",
"request": [...],
"response": {...},
"annotations": {
"0": {
"overall": {
"rating": "incorrect",
"comment": "Ответ неполный, не хватает информации о процентах"
},
"body_research": {
"issues": ["missing_info"],
"comment": "Не указаны процентные ставки"
}
},
"1": {
"overall": {
"rating": "correct",
"comment": "Ответ корректный"
}
}
},
"created_at": "2025-12-24T14:00:00Z",
"updated_at": "2025-12-24T14:30:00Z"
}
```
#### Errors
- **400 Bad Request**: Неверный формат данных
- **404 Not Found**: Сессия или пользователь не найдены
- **500 Internal Server Error**: Ошибка сервера
---
### DELETE /users/{user_id}/sessions/{session_id}
Удалить сессию.
#### Path Parameters
- `user_id` (UUID, required): UUID пользователя
- `session_id` (UUID, required): UUID сессии
#### Response 204
Нет тела ответа при успешном удалении.
#### Errors
- **404 Not Found**: Сессия или пользователь не найдены
- **500 Internal Server Error**: Ошибка сервера
---
## Форматы данных
### Environment Settings Object
```
{
"apiMode": "bench",
"bearerToken": "token-value",
"systemPlatform": "platform-name",
"systemPlatformUser": "user-name",
"platformUserId": "user-id",
"platformId": "platform-id",
"withClassify": false,
"resetSessionMode": true
}
```
**Поля:**
- `apiMode` (string): `bench` или `backend`
- `bearerToken` (string, nullable): Bearer токен для API
- `systemPlatform` (string, nullable): Название платформы
- `systemPlatformUser` (string, nullable): Пользователь платформы
- `platformUserId` (string, nullable): ID пользователя в платформе
- `platformId` (string, nullable): ID платформы
- `withClassify` (boolean): Использовать классификацию
- `resetSessionMode` (boolean): Режим сброса сессии
**Default значения:**
- `apiMode`: `"bench"`
- `withClassify`: `false`
- `resetSessionMode`: `true`
- Все остальные: `null`
---
## Обработка ошибок
Все ошибки возвращаются в едином формате:
```
{
"detail": "Описание ошибки",
"error_code": "OPTIONAL_ERROR_CODE"
}
```
### Коды ошибок
| HTTP Code | Описание | Когда возникает |
|-----------|----------|-----------------|
| 400 | Bad Request | Неверный формат данных в запросе |
| 404 | Not Found | Пользователь или сессия не найдены |
| 422 | Unprocessable Entity | Ошибка валидации Pydantic |
| 500 | Internal Server Error | Внутренняя ошибка сервера |
### Примеры ошибок
**400 - Неверный формат логина:**
```
{
"detail": "login: String should match pattern '^\\d{8}$'",
"error_code": null
}
```
**404 - Пользователь не найден:**
```
{
"detail": "User 550e8400-e29b-41d4-a716-446655440000 not found",
"error_code": null
}
```
**422 - Ошибка валидации annotations:**
```
{
"detail": [
{
"loc": ["body", "annotations"],
"msg": "annotations keys must be numeric strings (e.g. '0', '1')",
"type": "value_error"
}
]
}
```
---
## Примеры использования
### Сценарий 1: Создание пользователя и сохранение настроек
```
# 1. Логин пользователя
curl -X POST /api/v1/users/login \
-H "Content-Type: application/json" \
-d '{
"login": "12345678",
"client_ip": "MTkyLjE2OC4xLjEwMA=="
}'
# Response: {"user_id": "550e8400-...", ...}
# 2. Настройка окружения IFT
curl -X PATCH /api/v1/users/550e8400-e29b-41d4-a716-446655440000/settings \
-H "Content-Type: application/json" \
-d '{
"settings": {
"ift": {
"bearerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"systemPlatform": "platform-ift",
"withClassify": true
}
}
}'
```
### Сценарий 2: Создание и аннотация сессии
```
# 1. Создание сессии анализа
curl -X POST /api/v1/users/550e8400-e29b-41d4-a716-446655440000/sessions \
-H "Content-Type: application/json" \
-d '{
"environment": "ift",
"api_mode": "bench",
"request": [
{
"body": "Как получить кредит?",
"with_docs": true
}
],
"response": {
"answers": [{
"question": "Как получить кредит?",
"answer": "Для получения кредита..."
}]
}
}'
# Response: {"session_id": "660e8400-...", ...}
# 2. Добавление аннотаций после ревью
curl -X PATCH /api/v1/users/550e8400-e29b-41d4-a716-446655440000/sessions/660e8400-e29b-41d4-a716-446655440000 \
-H "Content-Type: application/json" \
-d '{
"annotations": {
"0": {
"overall": {
"rating": "correct",
"comment": "Ответ полный и корректный"
}
}
}
}'
```
### Сценарий 3: Получение истории сессий
```
# Список всех сессий пользователя
curl -X GET /api/v1/users/550e8400-e29b-41d4-a716-446655440000/sessions?limit=10&offset=0
# Фильтр только по IFT окружению
curl -X GET /api/v1/users/550e8400-e29b-41d4-a716-446655440000/sessions?environment=ift&limit=20
# Получение конкретной сессии
curl -X GET /api/v1/users/550e8400-e29b-41d4-a716-446655440000/sessions/660e8400-e29b-41d4-a716-446655440000
```
### Особенности реализации
1. **UUID генерация**: UUID создаются на уровне приложения (Python `uuid.uuid4()`)
2. **Timestamps**: Автоматическое проставление `load_dttm` при INSERT, `updated_dttm` при UPDATE через триггеры
3. **JSONB**: Все JSON-данные хранятся в PostgreSQL JSONB для эффективного поиска
4. **Индексы**: Составные индексы на `(user_id, environment, load_dttm)` для быстрой пагинации
5. **Безопасность**: Base64-кодирование client_ip для защиты от SQL-инъекций
---

View File

@ -7,7 +7,7 @@
## Текущее состояние
### Готово (Backend)
### Готово (Backend)
- Структура FastAPI приложения
- JWT авторизация (8-значный логин)
- TgBackendInterface (полная реализация с httpx)
@ -20,9 +20,9 @@
- `POST /api/v1/query/backend` - последовательные запросы
- `POST/GET/DELETE /api/v1/analysis/sessions` - сессии анализа
- Docker setup (Dockerfile, docker-compose.yml)
- Документация (README.md, DB_API_CONTRACT.md)
- Документация (README.md, DB_API_CONTRACT.md, CLAUDE.md)
### Требуется доделать
### Требуется доделать
- Frontend файлы (перенос из rag-bench-old-version)
- API client для frontend
- Интеграция frontend с новым API
@ -645,20 +645,20 @@ app.middleware("http")(log_requests)
## Приоритезация задач
### Критично (сделать в первую очередь)
### 🔴 Критично (сделать в первую очередь)
1. Перенос статических файлов из rag-bench-old-version → `static/`
2. Создание `api-client.js`
3. Добавление login screen в `index.html`
4. Переписывание вызовов API в `app.js`
5. Тестирование auth flow
### Важно (сделать после критичного)
### 🟡 Важно (сделать после критичного)
6. Интеграция Settings UI
7. Environment selector
8. Сохранение и загрузка сессий анализа
9. Ручное тестирование всех сценариев
### Желательно (если есть время)
### 🟢 Желательно (если есть время)
10. Автоматические тесты (pytest)
11. Production deployment настройка
12. Logging middleware

View File

@ -44,7 +44,7 @@ cp rag-bench-old-version/settings.js static/
## Шаг 2: Создание api-client.js
См. полную реализацию в DEVELOPMENT_PLAN.md (раздел "Создать API client для frontend").
См. полную реализацию в [DEVELOPMENT_PLAN.md](DEVELOPMENT_PLAN.md#12-создать-api-client-для-frontend).
Создать файл `static/api-client.js` с классом `BriefBenchAPI`.

View File

@ -1,60 +1,60 @@
# Production Readiness Checklist
# 🚀 Production Readiness Checklist
Полная проверка готовности Brief Bench FastAPI к развертыванию в продакшн.
## Backend (FastAPI)
## Backend (FastAPI)
### Код и архитектура
- [x] **Все API endpoints реализованы**
- Auth: `/api/v1/auth/login`
- Settings: GET/PUT `/api/v1/settings`
- Query: POST `/api/v1/query/bench`, `/api/v1/query/backend`
- Analysis: CRUD `/api/v1/analysis/sessions`
- Health: `/health`
- Auth: `/api/v1/auth/login`
- Settings: GET/PUT `/api/v1/settings`
- Query: POST `/api/v1/query/bench`, `/api/v1/query/backend`
- Analysis: CRUD `/api/v1/analysis/sessions`
- Health: `/health`
- [x] **Бизнес-логика покрыта тестами: 99%**
- 119 unit tests (99% coverage)
- Integration tests (DB API)
- E2E tests (полный стек)
- 119 unit tests (99% coverage)
- Integration tests (DB API)
- E2E tests (полный стек)
- [x] **Services реализованы**
- AuthService (JWT токены)
- RagService (RAG backends: IFT, PSI, PROD)
- AuthService (JWT токены)
- RagService (RAG backends: IFT, PSI, PROD)
- [x] **Interfaces реализованы**
- TgBackendInterface (базовый HTTP клиент)
- DBApiClient (DB API integration)
- TgBackendInterface (базовый HTTP клиент)
- DBApiClient (DB API integration)
- [x] **Models валидация**
- Все Pydantic models для request/response
- Валидация входных данных
- Все Pydantic models для request/response
- Валидация входных данных
## Frontend (Static Files)
## Frontend (Static Files)
- [x] **HTML/CSS/JS файлы**
- index.html
- styles.css (Material Design)
- app.js (основная логика)
- api-client.js (API клиент)
- settings.js (настройки)
- index.html
- styles.css (Material Design)
- app.js (основная логика)
- api-client.js (API клиент)
- settings.js (настройки)
- [x] **Интеграция с backend**
- API client использует `/api/v1` endpoints
- JWT токены в localStorage
- Правильная обработка ошибок (401, 502, etc.)
- StaticFiles монтированы в main.py
- API client использует `/api/v1` endpoints
- JWT токены в localStorage
- Правильная обработка ошибок (401, 502, etc.)
- StaticFiles монтированы в main.py
- [x] **UI функциональность**
- Login screen
- Multi-environment tabs (IFT, PSI, PROD)
- Settings panel
- Query interface
- Results display
- Session management
- Login screen
- Multi-environment tabs (IFT, PSI, PROD)
- Settings panel
- Query interface
- Results display
- Session management
## Конфигурация (ТРЕБУЕТ ВНИМАНИЯ!)
## ⚠️ Конфигурация (ТРЕБУЕТ ВНИМАНИЯ!)
### КРИТИЧНО - Сделать перед деплоем:
### 🔴 КРИТИЧНО - Сделать перед деплоем:
- [ ] **1. Создать `.env` файл**
```bash
@ -121,54 +121,55 @@
DEBUG=false
```
## Docker & Deployment
## Docker & Deployment
- [x] **Dockerfile готов**
- Multi-stage build
- Копирует static/ файлы
- Expose 8000
- Uvicorn с правильными параметрами
- Multi-stage build
- Копирует static/ файлы
- Expose 8000
- Uvicorn с правильными параметрами
- [x] **docker-compose.yml готов**
- Порты пробрасываются (8000:8000)
- Volume для certs (read-only)
- Volume для static файлов
- .env подключается
- restart: unless-stopped
- Порты пробрасываются (8000:8000)
- Volume для certs (read-only)
- Volume для static файлов
- .env подключается
- restart: unless-stopped
## Безопасность
## Безопасность
- [x] **Authentication**
- JWT токены (30 дней expiration)
- Bearer token authentication
- Middleware для проверки токенов
- JWT токены (30 дней expiration)
- Bearer token authentication
- Middleware для проверки токенов
- [x] **Secrets management**
- .env не в git (.gitignore)
- .env.integration не в git
- .env.e2e не в git
- ВАЖНО: Сменить JWT_SECRET_KEY в продакшн!
- .env не в git (.gitignore)
- .env.integration не в git
- .env.e2e не в git
- ⚠️ ВАЖНО: Сменить JWT_SECRET_KEY в продакшн!
- [x] **mTLS сертификаты**
- Хранятся только на сервере
- Read-only volume в Docker
- Не коммитятся в git
- Хранятся только на сервере
- Read-only volume в Docker
- Не коммитятся в git
- [ ] **HTTPS (рекомендуется)**
- Настроить reverse proxy (nginx/traefik)
- Let's Encrypt сертификаты
- Редирект HTTP → HTTPS
## Документация
## Документация
- [x] **README.md** - основная документация
- [x] **CLAUDE.md** - архитектура и гайд для Claude
- [x] **DB_API_CONTRACT.md** - контракт с DB API
- [x] **TESTING.md** - полное руководство по тестированию
- [x] **PROJECT_STATUS.md** - статус реализации
- [x] **tests/integration/README.md** - интеграционные тесты
- [x] **tests/e2e/README.md** - E2E тесты
## Pre-Deployment Testing
## 🔍 Pre-Deployment Testing
### Локальное тестирование
@ -237,7 +238,7 @@
docker-compose down
```
## Deployment Steps
## 🚀 Deployment Steps
### 1. Подготовка сервера
@ -307,7 +308,7 @@ server {
}
```
## Post-Deployment Verification
## 📊 Post-Deployment Verification
После деплоя проверить:
@ -319,7 +320,7 @@ server {
- [ ] Session save/load работает
- [ ] Логи не содержат ошибок: `docker-compose logs -f`
## Мониторинг и обслуживание
## 🔧 Мониторинг и обслуживание
### Логи
@ -356,19 +357,19 @@ docker-compose up -d --build
### Backup
Критичные данные:
- `.env` - секреты и конфигурация
- `certs/` - mTLS сертификаты
- Пользовательские данные хранятся в DB API (не в FastAPI)
- `.env` - секреты и конфигурация
- `certs/` - mTLS сертификаты
- Пользовательские данные хранятся в DB API (не в FastAPI)
## Performance Considerations
## Performance Considerations
- RAG запросы могут занимать до 30 минут (настроено)
- Async/await для всех I/O операций
- Connection pooling в httpx clients
- Рассмотреть rate limiting для production
- Рассмотреть caching для settings (опционально)
- RAG запросы могут занимать до 30 минут (настроено)
- Async/await для всех I/O операций
- Connection pooling в httpx clients
- Рассмотреть rate limiting для production
- Рассмотреть caching для settings (опционально)
## Troubleshooting
## 🐛 Troubleshooting
### Проблема: Контейнер не запускается
@ -399,30 +400,30 @@ docker-compose up -d --build
2. JWT_SECRET_KEY одинаковый между запусками
3. Токен не истек (30 дней по умолчанию)
## Final Checklist Summary
## Final Checklist Summary
Перед деплоем в продакшн:
1. Backend код готов (99% coverage)
2. Frontend интегрирован
3. Docker конфигурация готова
4. **`.env` создан и заполнен**
5. **`JWT_SECRET_KEY` сгенерирован новый**
6. **RAG hosts настроены**
7. **DB_API_URL настроен**
8. **mTLS сертификаты размещены** (если используются)
9. **CORS настроен** (при необходимости)
10. **DEBUG=false**
11. Unit тесты passed
12. Integration тесты passed (опционально)
13. Локальное тестирование пройдено
14. Docker build успешен
1. Backend код готов (99% coverage)
2. Frontend интегрирован
3. Docker конфигурация готова
4. ⚠️ **`.env` создан и заполнен**
5. ⚠️ **`JWT_SECRET_KEY` сгенерирован новый**
6. ⚠️ **RAG hosts настроены**
7. ⚠️ **DB_API_URL настроен**
8. ⚠️ **mTLS сертификаты размещены** (если используются)
9. ⚠️ **CORS настроен** (при необходимости)
10. ⚠️ **DEBUG=false**
11. Unit тесты passed
12. Integration тесты passed (опционально)
13. Локальное тестирование пройдено
14. Docker build успешен
---
**Статус готовности: ПОЧТИ ГОТОВ**
**Статус готовности: 🟡 ПОЧТИ ГОТОВ**
**Готово:** Код, тесты, Docker, документация
**Требуется:** Конфигурация окружения (.env, сертификаты, финальная настройка)
**Готово:** Код, тесты, Docker, документация
⚠️ **Требуется:** Конфигурация окружения (.env, сертификаты, финальная настройка)
После выполнения пунктов из раздела "КРИТИЧНО" → **ГОТОВ К ПРОДАКШН**
После выполнения пунктов из раздела "КРИТИЧНО" → **🟢 ГОТОВ К ПРОДАКШН**

View File

@ -5,7 +5,7 @@
---
## Что реализовано
## 📋 Что реализовано
### 1. Структура проекта
@ -15,45 +15,45 @@ brief-bench-fastapi/
│ ├── api/
│ │ └── v1/
│ │ ├── __init__.py
│ │ └── auth.py POST /api/v1/auth/login
│ │ └── auth.py POST /api/v1/auth/login
│ ├── models/
│ │ ├── __init__.py
│ │ ├── auth.py LoginRequest, LoginResponse, UserResponse
│ │ ├── settings.py EnvironmentSettings, UserSettings
│ │ ├── analysis.py SessionCreate, SessionResponse, SessionList
│ │ └── query.py BenchQueryRequest, BackendQueryRequest
│ │ ├── auth.py LoginRequest, LoginResponse, UserResponse
│ │ ├── settings.py EnvironmentSettings, UserSettings
│ │ ├── analysis.py SessionCreate, SessionResponse, SessionList
│ │ └── query.py BenchQueryRequest, BackendQueryRequest
│ ├── services/
│ │ ├── __init__.py
│ │ └── auth_service.py AuthService (login logic)
│ │ └── auth_service.py AuthService (login logic)
│ ├── interfaces/
│ │ ├── __init__.py
│ │ ├── base.py TgBackendInterface (ЗАГЛУШКА - нужна реализация)
│ │ └── db_api_client.py DBApiClient (методы для DB API)
│ │ ├── base.py ⚠️ TgBackendInterface (ЗАГЛУШКА - нужна реализация)
│ │ └── db_api_client.py DBApiClient (методы для DB API)
│ ├── middleware/
│ │ └── __init__.py
│ ├── utils/
│ │ ├── __init__.py
│ │ └── security.py JWT encode/decode
│ │ └── security.py JWT encode/decode
│ ├── __init__.py
│ ├── config.py Settings из .env
│ ├── dependencies.py DI: get_db_client, get_current_user
│ └── main.py FastAPI app с CORS
├── static/ Пусто (нужно скопировать из rag-bench)
├── tests/ Полный набор тестов (unit/integration/e2e)
├── certs/ Не создана (для mTLS)
├── .env.example
├── .gitignore
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── DB_API_CONTRACT.md Полный контракт для DB API
├── README.md
└── PROJECT_STATUS.md Этот файл
│ ├── config.py Settings из .env
│ ├── dependencies.py DI: get_db_client, get_current_user
│ └── main.py FastAPI app с CORS
├── static/ Пусто (нужно скопировать из rag-bench)
├── tests/ Полный набор тестов (unit/integration/e2e)
├── certs/ Не создана (для mTLS)
├── .env.example
├── .gitignore
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── DB_API_CONTRACT.md Полный контракт для DB API
├── README.md
└── PROJECT_STATUS.md Этот файл
```
---
## Реализованные компоненты
## Реализованные компоненты
### 1. Configuration (app/config.py)
- Загрузка из .env через pydantic-settings
@ -88,7 +88,7 @@ brief-bench-fastapi/
### 3. Interfaces (app/interfaces/)
**base.py (ЗАГЛУШКА!):**
**base.py (⚠️ ЗАГЛУШКА!):**
```python
class TgBackendInterface:
def __init__(self, api_prefix: str, **kwargs)
@ -152,7 +152,7 @@ class TgBackendInterface:
---
## Что НЕ реализовано (TODO)
## Что НЕ реализовано (TODO)
### 1. TgBackendInterface реализация (КРИТИЧНО!)
Файл: `app/interfaces/base.py`
@ -259,28 +259,28 @@ class RagService:
- `app/middleware/logging.py` - логирование запросов
- `app/middleware/error_handler.py` - глобальная обработка ошибок
### 9. Tests COMPLETED
- **Unit Tests** (119 tests, 99% coverage) - `tests/unit/`
### 9. Tests COMPLETED
- **Unit Tests** (119 tests, 99% coverage) - `tests/unit/`
- All services, models, utilities tested in isolation
- All external dependencies mocked
- Run: `.\run_unit_tests.bat`
- **Integration Tests** (DB API integration) - `tests/integration/`
- **Integration Tests** (DB API integration) - `tests/integration/`
- FastAPI endpoints with real DB API
- Requires DB API service running
- Run: `.\run_integration_tests.bat`
- **End-to-End Tests** (Full stack) - `tests/e2e/`
- **End-to-End Tests** (Full stack) - `tests/e2e/`
- Complete workflows: auth → query → save → retrieve
- Requires all services (FastAPI + DB API + RAG backends)
- Real network calls to RAG backends
- Run: `.\run_e2e_tests.bat`
- **Test Documentation** - `TESTING.md`
- **Test Documentation** - `TESTING.md`
- Comprehensive testing guide
- Setup instructions for each test level
- Troubleshooting and best practices
---
## Важные детали для продолжения
## 🔑 Важные детали для продолжения
### Архитектура авторизации
1. Пользователь отправляет POST /api/v1/auth/login?login=12345678
@ -346,7 +346,7 @@ Body: {
---
## План дальнейшей работы
## 🚀 План дальнейшей работы
### Этап 1: Реализовать TgBackendInterface
**Приоритет:** ВЫСОКИЙ
@ -398,7 +398,7 @@ app.include_router(analysis.router, prefix="/api/v1")
---
## Dependencies (requirements.txt)
## 📦 Dependencies (requirements.txt)
```
fastapi==0.104.1
@ -416,7 +416,7 @@ fastapi-cors==0.0.6
---
## Команды для разработки
## 🔧 Команды для разработки
```bash
# Установить зависимости
@ -436,7 +436,7 @@ curl http://localhost:8000/health
---
## Примечания
## 📝 Примечания
1. **TgBackendInterface** - это ваша реализация, которая будет использоваться во всех клиентах (DBApiClient, возможно RagClient в будущем)
@ -454,7 +454,7 @@ curl http://localhost:8000/health
---
## Готово к продолжению!
## 🎯 Готово к продолжению!
Вся базовая структура создана. Следующий шаг - реализация TgBackendInterface и остальных endpoints.

View File

@ -4,13 +4,13 @@ FastAPI backend для системы тестирования RAG с multi-user
## Возможности
- JWT авторизация (8-значный логин)
- Multi-environment: ИФТ, ПСИ, ПРОМ
- Bench mode: batch тестирование
- Backend mode: имитация бота (вопросы по одному)
- Сохранение сессий анализа
- mTLS для RAG backend
- Аннотации и экспорт
- 🔐 JWT авторизация (8-значный логин)
- 🌐 Multi-environment: ИФТ, ПСИ, ПРОМ
- 📊 Bench mode: batch тестирование
- 🤖 Backend mode: имитация бота (вопросы по одному)
- 💾 Сохранение сессий анализа
- 🔒 mTLS для RAG backend
- 📝 Аннотации и экспорт
## Требования
@ -186,6 +186,7 @@ docker-compose down
## Документация
- [CLAUDE.md](CLAUDE.md) - архитектура и гайд для разработки
- [TESTING.md](TESTING.md) - руководство по тестированию
- [PRODUCTION_CHECKLIST.md](PRODUCTION_CHECKLIST.md) - чек-лист для продакшн
- [DB_API_CONTRACT.md](DB_API_CONTRACT.md) - контракт с DB API
@ -193,13 +194,13 @@ docker-compose down
## Status
**Проект готов к продакшн**
**Проект готов к продакшн**
- Backend полностью реализован (все endpoints, services, interfaces)
- Frontend интегрирован (HTML/CSS/JS)
- 99% test coverage (unit + integration + E2E)
- Docker ready
- Требуется: настройка `.env` и сертификатов
- Backend полностью реализован (все endpoints, services, interfaces)
- Frontend интегрирован (HTML/CSS/JS)
- 99% test coverage (unit + integration + E2E)
- Docker ready
- ⚠️ Требуется: настройка `.env` и сертификатов
**Перед деплоем:** см. [PRODUCTION_CHECKLIST.md](PRODUCTION_CHECKLIST.md)

View File

@ -543,6 +543,7 @@ Current coverage:
- [Integration Tests](tests/integration/README.md) - DB API integration
- [E2E Tests](tests/e2e/README.md) - Full stack testing
- [DB API Contract](DB_API_CONTRACT.md) - External API spec
- [CLAUDE.md](CLAUDE.md) - Architecture overview
- [PROJECT_STATUS.md](PROJECT_STATUS.md) - Implementation status
## Summary

View File

@ -6,7 +6,7 @@ Analysis Sessions API endpoints.
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Optional
from app.models.analysis import SessionCreate, SessionResponse, SessionList, SessionUpdate
from app.models.analysis import SessionCreate, SessionResponse, SessionList
from app.interfaces.db_api_client import DBApiClient
from app.dependencies import get_db_client, get_current_user
import httpx
@ -143,54 +143,6 @@ async def get_session(
)
@router.patch("/sessions/{session_id}", response_model=SessionResponse)
async def update_session(
session_id: str,
update_data: SessionUpdate,
current_user: dict = Depends(get_current_user),
db_client: DBApiClient = Depends(get_db_client)
):
"""
Обновить аннотации сессии (например, после ревью).
Полностью заменяет существующие аннотации новыми.
Args:
session_id: UUID сессии
update_data: Новые аннотации с ключами в виде числовых строк ('0', '1', ...)
Returns:
SessionResponse: Обновленная сессия
"""
user_id = current_user["user_id"]
try:
updated_session = await db_client.update_session(user_id, session_id, update_data)
return updated_session
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Session not found"
)
elif e.response.status_code == 400:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid annotations format"
)
logger.error(f"Failed to update session {session_id}: {e}")
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Failed to update session in DB API"
)
except Exception as e:
logger.error(f"Unexpected error updating session {session_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
)
@router.delete("/sessions/{session_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_session(
session_id: str,

View File

@ -51,22 +51,20 @@ async def get_settings(
)
@router.patch("", response_model=UserSettings)
@router.put("", response_model=UserSettings)
async def update_settings(
settings_update: UserSettingsUpdate,
current_user: dict = Depends(get_current_user),
db_client: DBApiClient = Depends(get_db_client)
):
"""
Частично обновить настройки пользователя.
Обновляются только переданные поля. Непереданные поля остаются без изменений.
Обновить настройки пользователя.
Args:
settings_update: Частичные настройки для одного или нескольких окружений
settings_update: Новые настройки для одного или нескольких окружений
Returns:
UserSettings: Обновленные настройки со всеми полями
UserSettings: Обновленные настройки
"""
user_id = current_user["user_id"]

View File

@ -252,36 +252,6 @@ class TgBackendInterface:
response = await self.client.put(url, json=json_body, **kwargs)
return await self._handle_response(response, response_model)
async def patch(
self,
path: str,
body: Optional[BaseModel] = None,
response_model: Optional[Type[T]] = None,
**kwargs
) -> Any:
"""
HTTP PATCH запрос к {api_prefix}{path}.
Args:
path: Путь эндпоинта
body: Pydantic модель для тела запроса
response_model: Pydantic модель для валидации ответа
**kwargs: Дополнительные параметры для httpx
Returns:
Десериализованный ответ (Pydantic модель или dict)
Raises:
httpx.HTTPStatusError: При HTTP ошибках
ValidationError: При ошибках валидации Pydantic
"""
url = self._build_url(path)
json_body = self._serialize_body(body)
logger.debug(f"PATCH {url} with body={json_body}")
response = await self.client.patch(url, json=json_body, **kwargs)
return await self._handle_response(response, response_model)
async def delete(
self,
path: str,

View File

@ -3,7 +3,7 @@
from app.interfaces.base import TgBackendInterface
from app.models.auth import LoginRequest, UserResponse
from app.models.settings import UserSettings, UserSettingsUpdate
from app.models.analysis import SessionCreate, SessionResponse, SessionList, SessionUpdate
from app.models.analysis import SessionCreate, SessionResponse, SessionList
class DBApiClient(TgBackendInterface):
@ -11,7 +11,7 @@ class DBApiClient(TgBackendInterface):
Клиент для DB API сервиса.
Использует Pydantic схемы для type-safety.
Методы self.get(), self.post(), self.patch(), self.delete() от TgBackendInterface.
Методы self.get(), self.post(), self.put(), self.delete() от TgBackendInterface.
"""
async def login_user(self, request: LoginRequest) -> UserResponse:
@ -36,12 +36,11 @@ class DBApiClient(TgBackendInterface):
settings: UserSettingsUpdate
) -> UserSettings:
"""
PATCH {api_prefix}/users/{user_id}/settings
PUT {api_prefix}/users/{user_id}/settings
Частично обновить настройки пользователя.
Обновляются только переданные поля.
Обновить настройки пользователя.
"""
return await self.patch(
return await self.put(
f"/users/{user_id}/settings",
body=settings,
response_model=UserSettings
@ -95,23 +94,6 @@ class DBApiClient(TgBackendInterface):
response_model=SessionResponse
)
async def update_session(
self,
user_id: str,
session_id: str,
update_data: SessionUpdate
) -> SessionResponse:
"""
PATCH {api_prefix}/users/{user_id}/sessions/{session_id}
Обновить аннотации сессии (например, после ревью).
"""
return await self.patch(
f"/users/{user_id}/sessions/{session_id}",
body=update_data,
response_model=SessionResponse
)
async def delete_session(self, user_id: str, session_id: str) -> dict:
"""
DELETE {api_prefix}/users/{user_id}/sessions/{session_id}

View File

@ -1,29 +1,17 @@
"""Analysis session Pydantic models."""
from typing import Any, Optional
from pydantic import BaseModel, Field
from typing import Any
from pydantic import BaseModel
class SessionCreate(BaseModel):
"""Create new analysis session."""
environment: str = Field(..., description="Environment: ift, psi, or prod")
api_mode: str = Field(..., description="API mode: bench or backend")
request: list[Any] = Field(..., description="Array of request objects")
response: dict = Field(..., description="Response object (arbitrary structure)")
annotations: Optional[dict] = Field(default={}, description="Annotations by question index")
class SessionUpdate(BaseModel):
"""Update session annotations (PATCH).
According to DB_API_CONTRACT_V2.md:
- PATCH /users/{user_id}/sessions/{session_id}
- Used to update annotations after review
- Completely replaces existing annotations
"""
annotations: dict = Field(..., description="Annotations with numeric string keys ('0', '1', ...)")
environment: str
api_mode: str
request: list[Any]
response: dict
annotations: dict
class SessionResponse(BaseModel):

View File

@ -1,7 +1,7 @@
"""Query request/response Pydantic models."""
from typing import Any
from pydantic import BaseModel, Field
from pydantic import BaseModel
class QuestionRequest(BaseModel):
@ -26,42 +26,10 @@ class BackendQueryRequest(BaseModel):
reset_session: bool = True
class Docs(BaseModel):
"""Documents from RAG."""
research: list
analytical_hub: list
class RagResponse(BaseModel):
"""Ответ от RAG на вопрос пользователя."""
body_research: str = Field(description="Текст ответа от Research на вопрос")
body_analytical_hub: str = Field(description="Текст ответа от Analytical Hub на вопрос")
docs_from_vectorstore: Docs | None = None
docs_to_llm: Docs | None = None
class RagResponseBench(RagResponse):
"""Ответ на вопрос + время обработки именно этого вопроса."""
processing_time_sec: float = Field(
description="Время обработки запроса в секундах",
ge=0,
)
question: str = Field(description="Исходный вопрос")
class RagResponseBenchList(BaseModel):
"""Список ответов RAG в bench режиме."""
answers: list[RagResponseBench]
class QueryResponse(BaseModel):
"""Query response with metadata."""
request_id: str
timestamp: str
environment: str
response: RagResponseBenchList | dict | list # RagResponseBenchList для bench, dict/list для backend
response: dict

View File

@ -1,43 +1,21 @@
"""User settings Pydantic models."""
from typing import Optional
from pydantic import BaseModel, Field
from pydantic import BaseModel
class EnvironmentSettings(BaseModel):
"""Settings for a specific environment (IFT/PSI/PROD).
According to DB_API_CONTRACT_V2.md:
- apiMode, withClassify, resetSessionMode have defaults
- All other fields are nullable (Optional)
"""
"""Settings for a specific environment (IFT/PSI/PROD)."""
apiMode: str = "bench"
bearerToken: Optional[str] = None
systemPlatform: Optional[str] = None
systemPlatformUser: Optional[str] = None
platformUserId: Optional[str] = None
platformId: Optional[str] = None
bearerToken: str = ""
systemPlatform: str = ""
systemPlatformUser: str = ""
platformUserId: str = ""
platformId: str = ""
withClassify: bool = False
resetSessionMode: bool = True
class EnvironmentSettingsUpdate(BaseModel):
"""Partial update for environment settings (for PATCH requests).
All fields are optional to support partial updates.
"""
apiMode: Optional[str] = None
bearerToken: Optional[str] = None
systemPlatform: Optional[str] = None
systemPlatformUser: Optional[str] = None
platformUserId: Optional[str] = None
platformId: Optional[str] = None
withClassify: Optional[bool] = None
resetSessionMode: Optional[bool] = None
class UserSettings(BaseModel):
"""User settings for all environments."""
@ -47,10 +25,6 @@ class UserSettings(BaseModel):
class UserSettingsUpdate(BaseModel):
"""Partial update user settings request (PATCH).
"""Update user settings request."""
Only the environments/fields provided will be updated.
Unprovided fields remain unchanged.
"""
settings: dict[str, EnvironmentSettingsUpdate]
settings: dict[str, EnvironmentSettings]

View File

@ -12,7 +12,7 @@ import uuid
from typing import List, Dict, Optional, Any
from datetime import datetime
from app.config import settings
from app.models.query import QuestionRequest, RagResponseBenchList
from app.models.query import QuestionRequest
logger = logging.getLogger(__name__)
@ -200,7 +200,7 @@ class RagService:
questions: List[QuestionRequest],
user_settings: Dict,
request_id: Optional[str] = None
) -> RagResponseBenchList:
) -> Dict[str, Any]:
"""
Отправить batch запрос к RAG backend (bench mode).
@ -211,7 +211,7 @@ class RagService:
request_id: Request ID (опционально)
Returns:
RagResponseBenchList с ответом от RAG backend
Dict с ответом от RAG backend
Raises:
httpx.HTTPStatusError: При HTTP ошибках
@ -220,7 +220,7 @@ class RagService:
url = self._get_bench_endpoint(environment)
headers = self._build_bench_headers(environment, user_settings, request_id)
body = [q.model_dump() for q in questions]
logger.info(f"Sending bench query to {environment}: {len(questions)} questions")
@ -229,10 +229,7 @@ class RagService:
try:
response = await client.post(url, json=body, headers=headers)
response.raise_for_status()
response_data = response.json()
# Валидация ответа через Pydantic модель
return RagResponseBenchList(**response_data)
return response.json()
except httpx.HTTPStatusError as e:
logger.error(f"Bench query failed for {environment}: {e.response.status_code} - {e.response.text}")
raise

View File

@ -1,9 +1,9 @@
<?xml version="1.0" ?>
<coverage version="7.13.0" timestamp="1766645017585" lines-valid="617" lines-covered="594" line-rate="0.9627" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
<coverage version="7.13.0" timestamp="1766039646938" lines-valid="567" lines-covered="563" line-rate="0.9929" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.13.0 -->
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
<sources>
<source>C:\Users\itqop\Documents\code\brief-rags-bench\app</source>
<source>C:\Users\leonk\Documents\code\brief-bench-fastapi\app</source>
</sources>
<packages>
<package name="." line-rate="0.971" branch-rate="0" complexity="0">
@ -18,33 +18,33 @@
<line number="3" hits="1"/>
<line number="6" hits="1"/>
<line number="9" hits="1"/>
<line number="15" hits="1"/>
<line number="16" hits="1"/>
<line number="18" hits="1"/>
<line number="19" hits="1"/>
<line number="17" hits="1"/>
<line number="20" hits="1"/>
<line number="21" hits="1"/>
<line number="22" hits="1"/>
<line number="23" hits="1"/>
<line number="25" hits="1"/>
<line number="26" hits="1"/>
<line number="27" hits="1"/>
<line number="28" hits="1"/>
<line number="29" hits="1"/>
<line number="30" hits="1"/>
<line number="31" hits="1"/>
<line number="32" hits="1"/>
<line number="33" hits="1"/>
<line number="34" hits="1"/>
<line number="35" hits="1"/>
<line number="36" hits="1"/>
<line number="37" hits="1"/>
<line number="38" hits="1"/>
<line number="39" hits="1"/>
<line number="40" hits="1"/>
<line number="41" hits="1"/>
<line number="42" hits="1"/>
<line number="43" hits="1"/>
<line number="44" hits="1"/>
<line number="45" hits="1"/>
<line number="46" hits="1"/>
<line number="47" hits="1"/>
<line number="48" hits="1"/>
<line number="49" hits="1"/>
<line number="50" hits="1"/>
<line number="53" hits="1"/>
<line number="57" hits="1"/>
</lines>
</class>
<class name="dependencies.py" filename="dependencies.py" complexity="0" line-rate="1" branch-rate="0">
@ -75,25 +75,25 @@
<line number="6" hits="1"/>
<line number="8" hits="1"/>
<line number="9" hits="1"/>
<line number="11" hits="1"/>
<line number="17" hits="1"/>
<line number="25" hits="1"/>
<line number="26" hits="1"/>
<line number="27" hits="1"/>
<line number="12" hits="1"/>
<line number="19" hits="1"/>
<line number="28" hits="1"/>
<line number="29" hits="1"/>
<line number="30" hits="1"/>
<line number="33" hits="1"/>
<line number="31" hits="1"/>
<line number="34" hits="1"/>
<line number="36" hits="1"/>
<line number="39" hits="1"/>
<line number="37" hits="1"/>
<line number="38" hits="1"/>
<line number="40" hits="1"/>
<line number="42" hits="1"/>
<line number="45" hits="1"/>
<line number="43" hits="1"/>
<line number="44" hits="1"/>
<line number="46" hits="1"/>
<line number="48" hits="1"/>
<line number="51" hits="1"/>
<line number="52" hits="0"/>
<line number="53" hits="0"/>
<line number="49" hits="1"/>
<line number="50" hits="1"/>
<line number="52" hits="1"/>
<line number="55" hits="1"/>
<line number="56" hits="0"/>
<line number="57" hits="0"/>
</lines>
</class>
</classes>
@ -106,7 +106,7 @@
</class>
</classes>
</package>
<package name="api.v1" line-rate="0.9216" branch-rate="0" complexity="0">
<package name="api.v1" line-rate="0.9894" branch-rate="0" complexity="0">
<classes>
<class name="__init__.py" filename="api/v1/__init__.py" complexity="0" line-rate="1" branch-rate="0">
<methods/>
@ -115,7 +115,7 @@
<line number="7" hits="1"/>
</lines>
</class>
<class name="analysis.py" filename="api/v1/analysis.py" complexity="0" line-rate="0.8313" branch-rate="0">
<class name="analysis.py" filename="api/v1/analysis.py" complexity="0" line-rate="1" branch-rate="0">
<methods/>
<lines>
<line number="7" hits="1"/>
@ -173,34 +173,18 @@
<line number="140" hits="1"/>
<line number="146" hits="1"/>
<line number="147" hits="1"/>
<line number="165" hits="0"/>
<line number="167" hits="0"/>
<line number="168" hits="0"/>
<line number="169" hits="0"/>
<line number="170" hits="0"/>
<line number="171" hits="0"/>
<line number="172" hits="0"/>
<line number="176" hits="0"/>
<line number="177" hits="0"/>
<line number="181" hits="0"/>
<line number="182" hits="0"/>
<line number="186" hits="0"/>
<line number="187" hits="0"/>
<line number="188" hits="0"/>
<line number="194" hits="1"/>
<line number="195" hits="1"/>
<line number="209" hits="1"/>
<line number="211" hits="1"/>
<line number="212" hits="1"/>
<line number="213" hits="1"/>
<line number="214" hits="1"/>
<line number="215" hits="1"/>
<line number="216" hits="1"/>
<line number="220" hits="1"/>
<line number="221" hits="1"/>
<line number="225" hits="1"/>
<line number="226" hits="1"/>
<line number="227" hits="1"/>
<line number="161" hits="1"/>
<line number="163" hits="1"/>
<line number="164" hits="1"/>
<line number="165" hits="1"/>
<line number="166" hits="1"/>
<line number="167" hits="1"/>
<line number="168" hits="1"/>
<line number="172" hits="1"/>
<line number="173" hits="1"/>
<line number="177" hits="1"/>
<line number="178" hits="1"/>
<line number="179" hits="1"/>
</lines>
</class>
<class name="auth.py" filename="api/v1/auth.py" complexity="0" line-rate="1" branch-rate="0">
@ -321,31 +305,31 @@
<line number="48" hits="1"/>
<line number="54" hits="1"/>
<line number="55" hits="1"/>
<line number="69" hits="1"/>
<line number="71" hits="1"/>
<line number="72" hits="1"/>
<line number="73" hits="1"/>
<line number="74" hits="1"/>
<line number="75" hits="1"/>
<line number="76" hits="1"/>
<line number="77" hits="1"/>
<line number="78" hits="1"/>
<line number="82" hits="1"/>
<line number="83" hits="1"/>
<line number="87" hits="1"/>
<line number="88" hits="1"/>
<line number="80" hits="1"/>
<line number="81" hits="1"/>
<line number="85" hits="1"/>
<line number="86" hits="1"/>
<line number="90" hits="1"/>
<line number="91" hits="1"/>
<line number="92" hits="1"/>
<line number="93" hits="1"/>
<line number="94" hits="1"/>
</lines>
</class>
</classes>
</package>
<package name="interfaces" line-rate="0.9505" branch-rate="0" complexity="0">
<package name="interfaces" line-rate="1" branch-rate="0" complexity="0">
<classes>
<class name="__init__.py" filename="interfaces/__init__.py" complexity="0" line-rate="1" branch-rate="0">
<methods/>
<lines/>
</class>
<class name="base.py" filename="interfaces/base.py" complexity="0" line-rate="0.9351" branch-rate="0">
<class name="base.py" filename="interfaces/base.py" complexity="0" line-rate="1" branch-rate="0">
<methods/>
<lines>
<line number="9" hits="1"/>
@ -415,16 +399,10 @@
<line number="252" hits="1"/>
<line number="253" hits="1"/>
<line number="255" hits="1"/>
<line number="278" hits="0"/>
<line number="279" hits="0"/>
<line number="280" hits="0"/>
<line number="282" hits="0"/>
<line number="283" hits="0"/>
<line number="285" hits="1"/>
<line number="303" hits="1"/>
<line number="304" hits="1"/>
<line number="306" hits="1"/>
<line number="307" hits="1"/>
<line number="273" hits="1"/>
<line number="274" hits="1"/>
<line number="276" hits="1"/>
<line number="277" hits="1"/>
</lines>
</class>
<class name="db_api_client.py" filename="interfaces/db_api_client.py" complexity="0" line-rate="1" branch-rate="0">
@ -440,20 +418,18 @@
<line number="25" hits="1"/>
<line number="31" hits="1"/>
<line number="33" hits="1"/>
<line number="44" hits="1"/>
<line number="50" hits="1"/>
<line number="60" hits="1"/>
<line number="66" hits="1"/>
<line number="43" hits="1"/>
<line number="49" hits="1"/>
<line number="59" hits="1"/>
<line number="65" hits="1"/>
<line number="77" hits="1"/>
<line number="78" hits="1"/>
<line number="79" hits="1"/>
<line number="80" hits="1"/>
<line number="81" hits="1"/>
<line number="87" hits="1"/>
<line number="93" hits="1"/>
<line number="98" hits="1"/>
<line number="109" hits="1"/>
<line number="115" hits="1"/>
<line number="121" hits="1"/>
<line number="86" hits="1"/>
<line number="92" hits="1"/>
<line number="97" hits="1"/>
<line number="103" hits="1"/>
</lines>
</class>
</classes>
@ -484,24 +460,22 @@
<line number="13" hits="1"/>
<line number="14" hits="1"/>
<line number="17" hits="1"/>
<line number="20" hits="1"/>
<line number="21" hits="1"/>
<line number="22" hits="1"/>
<line number="23" hits="1"/>
<line number="24" hits="1"/>
<line number="25" hits="1"/>
<line number="26" hits="1"/>
<line number="29" hits="1"/>
<line number="32" hits="1"/>
<line number="33" hits="1"/>
<line number="27" hits="1"/>
<line number="28" hits="1"/>
<line number="31" hits="1"/>
<line number="34" hits="1"/>
<line number="35" hits="1"/>
<line number="36" hits="1"/>
<line number="37" hits="1"/>
<line number="38" hits="1"/>
<line number="39" hits="1"/>
<line number="40" hits="1"/>
<line number="42" hits="1"/>
<line number="43" hits="1"/>
<line number="46" hits="1"/>
<line number="47" hits="1"/>
<line number="48" hits="1"/>
<line number="51" hits="1"/>
<line number="54" hits="1"/>
<line number="55" hits="1"/>
</lines>
</class>
<class name="auth.py" filename="models/auth.py" complexity="0" line-rate="1" branch-rate="0">
@ -544,52 +518,29 @@
<line number="29" hits="1"/>
<line number="32" hits="1"/>
<line number="33" hits="1"/>
<line number="36" hits="1"/>
<line number="39" hits="1"/>
<line number="40" hits="1"/>
<line number="41" hits="1"/>
<line number="42" hits="1"/>
<line number="45" hits="1"/>
<line number="48" hits="1"/>
<line number="52" hits="1"/>
<line number="55" hits="1"/>
<line number="58" hits="1"/>
<line number="61" hits="1"/>
<line number="64" hits="1"/>
<line number="65" hits="1"/>
<line number="66" hits="1"/>
<line number="67" hits="1"/>
<line number="34" hits="1"/>
<line number="35" hits="1"/>
</lines>
</class>
<class name="settings.py" filename="models/settings.py" complexity="0" line-rate="1" branch-rate="0">
<methods/>
<lines>
<line number="3" hits="1"/>
<line number="4" hits="1"/>
<line number="7" hits="1"/>
<line number="6" hits="1"/>
<line number="9" hits="1"/>
<line number="10" hits="1"/>
<line number="11" hits="1"/>
<line number="12" hits="1"/>
<line number="13" hits="1"/>
<line number="14" hits="1"/>
<line number="15" hits="1"/>
<line number="16" hits="1"/>
<line number="17" hits="1"/>
<line number="18" hits="1"/>
<line number="19" hits="1"/>
<line number="20" hits="1"/>
<line number="21" hits="1"/>
<line number="22" hits="1"/>
<line number="25" hits="1"/>
<line number="31" hits="1"/>
<line number="32" hits="1"/>
<line number="33" hits="1"/>
<line number="34" hits="1"/>
<line number="35" hits="1"/>
<line number="36" hits="1"/>
<line number="37" hits="1"/>
<line number="38" hits="1"/>
<line number="41" hits="1"/>
<line number="44" hits="1"/>
<line number="45" hits="1"/>
<line number="46" hits="1"/>
<line number="49" hits="1"/>
<line number="56" hits="1"/>
<line number="23" hits="1"/>
<line number="24" hits="1"/>
<line number="27" hits="1"/>
<line number="30" hits="1"/>
</lines>
</class>
</classes>
@ -698,41 +649,40 @@
<line number="230" hits="1"/>
<line number="231" hits="1"/>
<line number="232" hits="1"/>
<line number="233" hits="1"/>
<line number="234" hits="1"/>
<line number="235" hits="1"/>
<line number="236" hits="1"/>
<line number="237" hits="1"/>
<line number="238" hits="1"/>
<line number="239" hits="1"/>
<line number="240" hits="1"/>
<line number="241" hits="1"/>
<line number="243" hits="1"/>
<line number="267" hits="1"/>
<line number="264" hits="1"/>
<line number="265" hits="1"/>
<line number="266" hits="1"/>
<line number="268" hits="1"/>
<line number="269" hits="1"/>
<line number="271" hits="1"/>
<line number="276" hits="1"/>
<line number="273" hits="1"/>
<line number="275" hits="1"/>
<line number="277" hits="1"/>
<line number="278" hits="1"/>
<line number="280" hits="1"/>
<line number="281" hits="1"/>
<line number="288" hits="1"/>
<line number="285" hits="1"/>
<line number="287" hits="1"/>
<line number="289" hits="1"/>
<line number="290" hits="1"/>
<line number="291" hits="1"/>
<line number="292" hits="1"/>
<line number="293" hits="1"/>
<line number="294" hits="1"/>
<line number="295" hits="1"/>
<line number="296" hits="1"/>
<line number="297" hits="1"/>
<line number="298" hits="1"/>
<line number="299" hits="1"/>
<line number="300" hits="1"/>
<line number="301" hits="1"/>
<line number="303" hits="1"/>
<line number="305" hits="1"/>
<line number="306" hits="1"/>
<line number="308" hits="1"/>
<line number="309" hits="1"/>
<line number="310" hits="1"/>
<line number="311" hits="1"/>
<line number="312" hits="1"/>
<line number="313" hits="1"/>
<line number="314" hits="1"/>
<line number="315" hits="1"/>
<line number="316" hits="1"/>
<line number="318" hits="1"/>
<line number="319" hits="1"/>
</lines>
</class>
</classes>

View File

@ -1,27 +0,0 @@
from pydantic import BaseModel, Field
class Docs(BaseModel):
research: list
analytical_hub: list
class RagResponse(BaseModel):
"""Ответ от RAG на вопрос пользователя."""
body_research: str = Field(description="Текст ответа от Research на вопрос")
body_analytical_hub: str = Field(description="Текст ответа от Analytical Hub на вопрос")
docs_from_vectorstore: Docs | None = None
docs_to_llm: Docs | None = None
class RagResponseBench(RagResponse):
"""Ответ на вопрос + время обработки именно этого вопроса."""
processing_time_sec: float = Field(
description="Время обработки запроса в секундах",
ge=0,
)
question: str = Field(description="Исходный вопрос")
class RagResponseBenchList(BaseModel):
answers: list[RagResponseBench]

View File

@ -107,7 +107,7 @@ pytest -x
pytest -s
```
## Покрытие тестами
## Покрытие (Coverage)
### Unit Tests: **99%** (567 строк, 4 непокрыто)
@ -133,51 +133,51 @@ pytest -s
## Что тестируется
### 1. Authentication (test_auth.py)
- Успешная авторизация с валидным 8-значным логином
- Отклонение невалидных форматов логина
- Обработка ошибок DB API
- Генерация JWT токенов
- Валидация токенов
- Успешная авторизация с валидным 8-значным логином
- Отклонение невалидных форматов логина
- Обработка ошибок DB API
- Генерация JWT токенов
- Валидация токенов
### 2. Settings (test_settings.py)
- Получение настроек пользователя
- Обновление настроек
- Обработка несуществующих пользователей
- Валидация формата настроек
- Требование авторизации
- Получение настроек пользователя
- Обновление настроек
- Обработка несуществующих пользователей
- Валидация формата настроек
- Требование авторизации
### 3. Query (test_query.py)
- Bench mode запросы
- Backend mode запросы
- Валидация окружений (ift/psi/prod)
- Проверка соответствия apiMode
- Обработка ошибок RAG backend
- Построение headers для RAG
- Session reset в Backend mode
- Bench mode запросы
- Backend mode запросы
- Валидация окружений (ift/psi/prod)
- Проверка соответствия apiMode
- Обработка ошибок RAG backend
- Построение headers для RAG
- Session reset в Backend mode
### 4. Analysis (test_analysis.py)
- Создание сессий анализа
- Получение списка сессий
- Фильтрация по окружению
- Пагинация
- Получение конкретной сессии
- Удаление сессии
- Требование авторизации
- Создание сессий анализа
- Получение списка сессий
- Фильтрация по окружению
- Пагинация
- Получение конкретной сессии
- Удаление сессии
- Требование авторизации
### 5. Security (test_security.py)
- Создание JWT токенов
- Декодирование токенов
- Обработка невалидных токенов
- Обработка истекших токенов
- Кастомное время жизни токенов
- Создание JWT токенов
- Декодирование токенов
- Обработка невалидных токенов
- Обработка истекших токенов
- Кастомное время жизни токенов
### 6. Models (test_models.py)
- Валидация LoginRequest (8 цифр)
- Валидация QuestionRequest
- Валидация BenchQueryRequest
- Валидация BackendQueryRequest
- Валидация EnvironmentSettings
- Дефолтные значения
- Валидация LoginRequest (8 цифр)
- Валидация QuestionRequest
- Валидация BenchQueryRequest
- Валидация BackendQueryRequest
- Валидация EnvironmentSettings
- Дефолтные значения
## Моки

View File

@ -144,36 +144,20 @@ def unauthenticated_client():
@pytest.fixture
def mock_bench_response():
"""Mock RAG backend bench response (matches format.py structure)."""
"""Mock RAG backend bench response."""
return {
"answers": [
{
"body_research": "Test research answer 1",
"body_analytical_hub": "Test analytical hub answer 1",
"docs_from_vectorstore": {
"research": [],
"analytical_hub": []
},
"docs_to_llm": {
"research": [],
"analytical_hub": []
},
"processing_time_sec": 1.5,
"question": "Test question 1"
"question_id": 1,
"answer": "Test answer 1",
"confidence": 0.95,
"docs": []
},
{
"body_research": "Test research answer 2",
"body_analytical_hub": "Test analytical hub answer 2",
"docs_from_vectorstore": {
"research": [],
"analytical_hub": []
},
"docs_to_llm": {
"research": [],
"analytical_hub": []
},
"processing_time_sec": 2.3,
"question": "Test question 2"
"question_id": 2,
"answer": "Test answer 2",
"confidence": 0.87,
"docs": []
}
]
}

View File

@ -435,3 +435,4 @@ When adding new E2E tests:
- [Integration Tests](../integration/README.md) - Tests for DB API integration only
- [Unit Tests](../unit/) - Fast isolated tests
- [DB API Contract](../../DB_API_CONTRACT.md) - External DB API specification
- [CLAUDE.md](../../CLAUDE.md) - Project architecture overview

View File

@ -88,20 +88,20 @@ tests/integration/
## Что тестируется
### Auth Integration (`test_auth_integration.py`)
### Auth Integration (`test_auth_integration.py`)
- Успешная авторизация с реальным DB API
- Генерация и валидация JWT токенов
- Защита endpoint-ов с использованием JWT
- Обработка ошибок аутентификации
### Settings Integration (`test_settings_integration.py`)
### Settings Integration (`test_settings_integration.py`)
- Получение настроек пользователя из DB API
- Обновление настроек для всех окружений (IFT, PSI, PROD)
- Частичное обновление настроек
- Персистентность настроек
- Проверка структуры данных настроек
### Analysis Integration (`test_analysis_integration.py`)
### Analysis Integration (`test_analysis_integration.py`)
- Создание сессий анализа в DB API
- Получение списка сессий с фильтрацией
- Пагинация сессий
@ -109,7 +109,7 @@ tests/integration/
- Удаление сессий
- Целостность данных (включая Unicode, вложенные структуры)
### Query Integration (`test_query_integration.py`)
### Query Integration (`test_query_integration.py`)
- Получение настроек пользователя для запросов
- Проверка соответствия apiMode (bench/backend)
- Обновление настроек между запросами
@ -117,10 +117,10 @@ tests/integration/
## Что НЕ тестируется
**RAG Backend взаимодействие** - требует запущенные RAG сервисы (IFT/PSI/PROD)
**mTLS сертификаты** - требует реальные сертификаты
**Производительность** - используйте отдельные performance тесты
**Нагрузочное тестирование** - используйте инструменты типа Locust/K6
**RAG Backend взаимодействие** - требует запущенные RAG сервисы (IFT/PSI/PROD)
**mTLS сертификаты** - требует реальные сертификаты
**Производительность** - используйте отдельные performance тесты
**Нагрузочное тестирование** - используйте инструменты типа Locust/K6
## Troubleshooting

View File

@ -4,8 +4,8 @@ import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from app.interfaces.db_api_client import DBApiClient
from app.models.auth import LoginRequest, UserResponse
from app.models.settings import UserSettings, UserSettingsUpdate, EnvironmentSettings, EnvironmentSettingsUpdate
from app.models.analysis import SessionCreate, SessionResponse, SessionList, SessionListItem, SessionUpdate
from app.models.settings import UserSettings, UserSettingsUpdate, EnvironmentSettings
from app.models.analysis import SessionCreate, SessionResponse, SessionList, SessionListItem
class TestDBApiClient:
@ -70,14 +70,19 @@ class TestDBApiClient:
@pytest.mark.asyncio
async def test_update_user_settings(self):
"""Test update_user_settings calls patch correctly."""
"""Test update_user_settings calls put correctly."""
with patch('app.interfaces.base.httpx.AsyncClient'):
client = DBApiClient(api_prefix="http://db-api:8080/api/v1")
settings_update = UserSettingsUpdate(
settings={
"ift": EnvironmentSettingsUpdate(
"ift": EnvironmentSettings(
apiMode="backend",
bearerToken="",
systemPlatform="",
systemPlatformUser="",
platformUserId="",
platformId="",
withClassify=True,
resetSessionMode=False
)
@ -85,26 +90,15 @@ class TestDBApiClient:
)
mock_updated_settings = UserSettings(
user_id="user-123",
settings={
"ift": EnvironmentSettings(
apiMode="backend",
bearerToken=None,
systemPlatform=None,
systemPlatformUser=None,
platformUserId=None,
platformId=None,
withClassify=True,
resetSessionMode=False
)
},
settings=settings_update.settings,
updated_at="2024-01-01T01:00:00Z"
)
client.patch = AsyncMock(return_value=mock_updated_settings)
client.put = AsyncMock(return_value=mock_updated_settings)
result = await client.update_user_settings("user-123", settings_update)
assert result == mock_updated_settings
client.patch.assert_called_once_with(
client.put.assert_called_once_with(
"/users/user-123/settings",
body=settings_update,
response_model=UserSettings
@ -217,44 +211,6 @@ class TestDBApiClient:
response_model=SessionResponse
)
@pytest.mark.asyncio
async def test_update_session(self):
"""Test update_session calls patch correctly."""
with patch('app.interfaces.base.httpx.AsyncClient'):
client = DBApiClient(api_prefix="http://db-api:8080/api/v1")
update_data = SessionUpdate(
annotations={
"0": {
"overall": {
"rating": "incorrect",
"comment": "Ответ неполный"
}
}
}
)
mock_updated_session = SessionResponse(
session_id="session-123",
user_id="user-123",
environment="ift",
api_mode="bench",
request=[{"question": "test"}],
response={"answer": "test"},
annotations=update_data.annotations,
created_at="2024-01-01T00:00:00Z",
updated_at="2024-01-01T01:00:00Z"
)
client.patch = AsyncMock(return_value=mock_updated_session)
result = await client.update_session("user-123", "session-123", update_data)
assert result == mock_updated_session
client.patch.assert_called_once_with(
"/users/user-123/sessions/session-123",
body=update_data,
response_model=SessionResponse
)
@pytest.mark.asyncio
async def test_delete_session(self):
"""Test delete_session calls delete correctly."""

View File

@ -4,8 +4,7 @@ import pytest
from pydantic import ValidationError
from app.models.auth import LoginRequest, UserResponse, LoginResponse
from app.models.query import QuestionRequest, BenchQueryRequest, BackendQueryRequest, QueryResponse
from app.models.settings import EnvironmentSettings, EnvironmentSettingsUpdate, UserSettingsUpdate
from app.models.analysis import SessionCreate, SessionUpdate
from app.models.settings import EnvironmentSettings, UserSettingsUpdate
class TestAuthModels:
@ -121,34 +120,16 @@ class TestQueryModels:
def test_query_response(self):
"""Test QueryResponse model."""
# Test with RagResponseBenchList (parsed from dict)
response = QueryResponse(
request_id="req-123",
timestamp="2024-01-01T00:00:00Z",
environment="ift",
response={"answers": []} # Auto-parsed to RagResponseBenchList
response={"answers": []}
)
assert response.request_id == "req-123"
assert response.environment == "ift"
# When dict with "answers" is provided, it's parsed as RagResponseBenchList
from app.models.query import RagResponseBenchList
assert isinstance(response.response, RagResponseBenchList)
def test_query_response_with_dict(self):
"""Test QueryResponse model with plain dict (backend mode)."""
# Use dict that doesn't match RagResponseBenchList schema
response = QueryResponse(
request_id="req-456",
timestamp="2024-01-01T00:00:00Z",
environment="psi",
response={"result": "some data", "status": "ok"}
)
assert response.request_id == "req-456"
assert response.environment == "psi"
assert isinstance(response.response, dict)
assert response.response["status"] == "ok"
class TestSettingsModels:
@ -173,135 +154,30 @@ class TestSettingsModels:
assert settings.resetSessionMode is False
def test_environment_settings_defaults(self):
"""Test EnvironmentSettings with default values (nullable fields)."""
settings = EnvironmentSettings()
"""Test EnvironmentSettings with default values."""
settings = EnvironmentSettings(apiMode="backend")
assert settings.apiMode == "bench"
assert settings.bearerToken is None
assert settings.systemPlatform is None
assert settings.systemPlatformUser is None
assert settings.platformUserId is None
assert settings.platformId is None
assert settings.apiMode == "backend"
assert settings.bearerToken == ""
assert settings.withClassify is False
assert settings.resetSessionMode is True
def test_environment_settings_nullable_fields(self):
"""Test EnvironmentSettings with explicit None values."""
settings = EnvironmentSettings(
apiMode="backend",
bearerToken=None,
systemPlatform=None,
withClassify=True
)
assert settings.apiMode == "backend"
assert settings.bearerToken is None
assert settings.systemPlatform is None
assert settings.withClassify is True
def test_environment_settings_update_partial(self):
"""Test EnvironmentSettingsUpdate for partial updates."""
update = EnvironmentSettingsUpdate(
bearerToken="new-token",
withClassify=True
)
assert update.bearerToken == "new-token"
assert update.withClassify is True
assert update.apiMode is None # Not provided, should be None
assert update.systemPlatform is None
def test_environment_settings_update_all_none(self):
"""Test EnvironmentSettingsUpdate with all fields as None."""
update = EnvironmentSettingsUpdate()
assert update.apiMode is None
assert update.bearerToken is None
assert update.systemPlatform is None
assert update.withClassify is None
def test_user_settings_update(self):
"""Test UserSettingsUpdate model with partial updates."""
"""Test UserSettingsUpdate model."""
update = UserSettingsUpdate(
settings={
"ift": EnvironmentSettingsUpdate(apiMode="bench", withClassify=True),
"psi": EnvironmentSettingsUpdate(bearerToken="token123")
"ift": EnvironmentSettings(apiMode="bench"),
"psi": EnvironmentSettings(apiMode="backend")
}
)
assert "ift" in update.settings
assert "psi" in update.settings
assert update.settings["ift"].apiMode == "bench"
assert update.settings["ift"].withClassify is True
assert update.settings["psi"].bearerToken == "token123"
assert update.settings["psi"].apiMode is None # Not provided
assert update.settings["psi"].apiMode == "backend"
def test_user_settings_update_empty(self):
"""Test UserSettingsUpdate with empty settings."""
update = UserSettingsUpdate(settings={})
assert update.settings == {}
class TestAnalysisModels:
"""Tests for analysis session models."""
def test_session_create_valid(self):
"""Test valid SessionCreate."""
session = SessionCreate(
environment="ift",
api_mode="bench",
request=[{"body": "question 1", "with_docs": True}],
response={"answers": ["answer 1"]},
annotations={"0": {"rating": "correct"}}
)
assert session.environment == "ift"
assert session.api_mode == "bench"
assert len(session.request) == 1
assert session.annotations == {"0": {"rating": "correct"}}
def test_session_create_default_annotations(self):
"""Test SessionCreate with default empty annotations."""
session = SessionCreate(
environment="psi",
api_mode="backend",
request=[],
response={}
)
assert session.annotations == {}
def test_session_update_valid(self):
"""Test valid SessionUpdate."""
update = SessionUpdate(
annotations={
"0": {
"overall": {
"rating": "incorrect",
"comment": "Ответ неполный"
},
"body_research": {
"issues": ["missing_info"],
"comment": "Не указаны процентные ставки"
}
},
"1": {
"overall": {
"rating": "correct",
"comment": "Ответ корректный"
}
}
}
)
assert "0" in update.annotations
assert "1" in update.annotations
assert update.annotations["0"]["overall"]["rating"] == "incorrect"
assert update.annotations["1"]["overall"]["rating"] == "correct"
def test_session_update_empty_annotations(self):
"""Test SessionUpdate with empty annotations."""
update = SessionUpdate(annotations={})
assert update.annotations == {}

View File

@ -4,7 +4,7 @@ import pytest
from unittest.mock import AsyncMock, patch, MagicMock
import httpx
from app.services.rag_service import RagService
from app.models.query import QuestionRequest, RagResponseBenchList
from app.models.query import QuestionRequest
class TestBenchQueryEndpoint:
@ -16,9 +16,7 @@ class TestBenchQueryEndpoint:
with patch('app.api.v1.query.RagService') as MockRagService:
mock_rag = AsyncMock()
# Mock возвращает RagResponseBenchList
from app.models.query import RagResponseBenchList
mock_rag.send_bench_query = AsyncMock(return_value=RagResponseBenchList(**mock_bench_response))
mock_rag.send_bench_query = AsyncMock(return_value=mock_bench_response)
mock_rag.close = AsyncMock()
MockRagService.return_value = mock_rag
@ -39,9 +37,7 @@ class TestBenchQueryEndpoint:
assert "timestamp" in data
assert data["environment"] == "ift"
assert "response" in data
# FastAPI автоматически сериализует RagResponseBenchList в dict
assert data["response"]["answers"][0]["question"] == "Test question 1"
assert data["response"]["answers"][0]["body_research"] == "Test research answer 1"
assert data["response"] == mock_bench_response
mock_rag.send_bench_query.assert_called_once()
mock_rag.close.assert_called_once()
@ -249,7 +245,7 @@ class TestRagService:
@pytest.mark.asyncio
async def test_send_bench_query_success(self, mock_httpx_client, mock_bench_response):
"""Test successful bench query via RagService."""
mock_httpx_client.post.return_value.json.return_value = mock_bench_response
with patch('app.services.rag_service.httpx.AsyncClient', return_value=mock_httpx_client):
@ -272,15 +268,10 @@ class TestRagService:
request_id="test-request-123"
)
# Проверяем, что результат - это RagResponseBenchList
assert isinstance(result, RagResponseBenchList)
assert len(result.answers) == 2
assert result.answers[0].question == "Test question 1"
assert result.answers[0].body_research == "Test research answer 1"
assert result == mock_bench_response
mock_httpx_client.post.assert_called_once()
call_kwargs = mock_httpx_client.post.call_args[1]
headers = call_kwargs["headers"]
assert headers["Request-Id"] == "test-request-123"

View File

@ -46,7 +46,7 @@ class TestSettingsEndpoints:
assert response.status_code == 401
def test_update_settings_success(self, client, mock_db_client, test_settings):
"""Test updating user settings successfully with PATCH (partial update)."""
"""Test updating user settings successfully."""
mock_db_client.update_user_settings = AsyncMock(return_value=test_settings)
update_data = {
@ -59,7 +59,7 @@ class TestSettingsEndpoints:
}
}
response = client.patch("/api/v1/settings", json=update_data)
response = client.put("/api/v1/settings", json=update_data)
assert response.status_code == 200
data = response.json()
@ -80,7 +80,7 @@ class TestSettingsEndpoints:
}
}
response = client.patch("/api/v1/settings", json=update_data)
response = client.put("/api/v1/settings", json=update_data)
assert response.status_code == 400
@ -96,7 +96,7 @@ class TestSettingsEndpoints:
}
}
response = client.patch("/api/v1/settings", json=update_data)
response = client.put("/api/v1/settings", json=update_data)
assert response.status_code == 500
@ -136,7 +136,7 @@ class TestSettingsEndpoints:
}
}
response = client.patch("/api/v1/settings", json=update_data)
response = client.put("/api/v1/settings", json=update_data)
assert response.status_code == 404
assert "user not found" in response.json()["detail"].lower()
@ -154,26 +154,7 @@ class TestSettingsEndpoints:
}
}
response = client.patch("/api/v1/settings", json=update_data)
response = client.put("/api/v1/settings", json=update_data)
assert response.status_code == 502
assert "failed to update settings" in response.json()["detail"].lower()
def test_update_settings_partial_update(self, client, mock_db_client, test_settings):
"""Test partial update - only updating specific fields."""
mock_db_client.update_user_settings = AsyncMock(return_value=test_settings)
# Partial update - only bearerToken and withClassify for IFT
update_data = {
"settings": {
"ift": {
"bearerToken": "updated-token-123",
"withClassify": True
}
}
}
response = client.patch("/api/v1/settings", json=update_data)
assert response.status_code == 200
mock_db_client.update_user_settings.assert_called_once()