1215 lines
44 KiB
Markdown
1215 lines
44 KiB
Markdown
```python
|
||||
|
|
import os
|
|||
|
|
import zipfile
|
|||
|
|
|
|||
|
|
# Prepare the project files
|
|||
|
|
project_structure = {
|
|||
|
|
"requirements.txt": """pytest>=8.0.0
|
|||
|
|
requests>=2.31.0
|
|||
|
|
python-dotenv>=1.0.0
|
|||
|
|
allure-pytest>=2.13.0
|
|||
|
|
""",
|
|||
|
|
".env.example": """# OAuth-токен от Яндекс Полигона / тестового аккаунта
|
|||
|
|
# Получить токен можно на странице полигона API Яндекс Диска:
|
|||
|
|
# https://yandex.ru/dev/disk/poligon/
|
|||
|
|
YANDEX_DISK_TOKEN=y0_AgAAAA...
|
|||
|
|
""",
|
|||
|
|
".gitignore": """# Python artifacts
|
|||
|
|
__pycache__/
|
|||
|
|
*.py[cod]
|
|||
|
|
*$py.class
|
|||
|
|
*.so
|
|||
|
|
.Python
|
|||
|
|
build/
|
|||
|
|
develop-eggs/
|
|||
|
|
dist/
|
|||
|
|
downloads/
|
|||
|
|
eggs/
|
|||
|
|
.eggs/
|
|||
|
|
lib/
|
|||
|
|
lib64/
|
|||
|
|
parts/
|
|||
|
|
sdist/
|
|||
|
|
var/
|
|||
|
|
wheels/
|
|||
|
|
*.egg-info/
|
|||
|
|
.installed.cfg
|
|||
|
|
*.egg
|
|||
|
|
|
|||
|
|
# Virtual environments
|
|||
|
|
venv/
|
|||
|
|
.venv/
|
|||
|
|
env/
|
|||
|
|
ENV/
|
|||
|
|
|
|||
|
|
# Environment variables & secrets
|
|||
|
|
.env
|
|||
|
|
.env.local
|
|||
|
|
|
|||
|
|
# IDE & editors
|
|||
|
|
.vscode/
|
|||
|
|
.idea/
|
|||
|
|
*.swp
|
|||
|
|
*.swo
|
|||
|
|
|
|||
|
|
# Testing & coverage
|
|||
|
|
.pytest_cache/
|
|||
|
|
.coverage
|
|||
|
|
htmlcov/
|
|||
|
|
allure-results/
|
|||
|
|
allure-report/
|
|||
|
|
.cache/
|
|||
|
|
""",
|
|||
|
|
"pytest.ini": """[pytest]
|
|||
|
|
testpaths = tests
|
|||
|
|
python_files = test_*.py
|
|||
|
|
python_classes = Test*
|
|||
|
|
python_functions = test_*
|
|||
|
|
addopts = -v -s --tb=short
|
|||
|
|
markers =
|
|||
|
|
crud: CRUD operations tests
|
|||
|
|
negative: Negative tests
|
|||
|
|
""",
|
|||
|
|
"api/__init__.py": "",
|
|||
|
|
"api/disk_client.py": '''"""
|
|||
|
|
Модуль клиента для взаимодействия с REST API Яндекс Диска.
|
|||
|
|
Документация API: https://yandex.ru/dev/disk/api/concepts/about.html
|
|||
|
|
Полигон: https://cloud-api.yandex.net
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import requests
|
|||
|
|
from typing import Optional, Dict, Any
|
|||
|
|
|
|||
|
|
|
|||
|
|
class YandexDiskClient:
|
|||
|
|
"""
|
|||
|
|
HTTP-клиент-обёртка для выполнения запросов к API Яндекс Диска.
|
|||
|
|
Инкапсулирует базовые методы (GET, POST, PUT, DELETE) и авторизацию.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
BASE_URL = "https://cloud-api.yandex.net/v1/disk/resources"
|
|||
|
|
|
|||
|
|
def __init__(self, token: str, timeout: int = 10):
|
|||
|
|
"""
|
|||
|
|
Инициализация сессии с предустановленными заголовками авторизации.
|
|||
|
|
|
|||
|
|
:param token: OAuth-токен сервиса Яндекс
|
|||
|
|
:param timeout: Дефолтный таймаут для HTTP-запросов в секундах
|
|||
|
|
"""
|
|||
|
|
self.timeout = timeout
|
|||
|
|
self.session = requests.Session()
|
|||
|
|
# Все запросы к API Яндекс Диска требуют заголовок Authorization с префиксом OAuth
|
|||
|
|
self.session.headers.update({
|
|||
|
|
"Authorization": f"OAuth {token}",
|
|||
|
|
"Accept": "application/json",
|
|||
|
|
"Content-Type": "application/json"
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
def create_folder(self, path: str) -> requests.Response:
|
|||
|
|
"""
|
|||
|
|
PUT /v1/disk/resources
|
|||
|
|
Создание папки на Яндекс Диске.
|
|||
|
|
|
|||
|
|
:param path: Путь к создаваемой папке (например, 'test_dir' или 'disk:/test_dir')
|
|||
|
|
:return: Объект ответа requests.Response (201 Created при успехе)
|
|||
|
|
"""
|
|||
|
|
url = self.BASE_URL
|
|||
|
|
params = {"path": path}
|
|||
|
|
return self.session.put(url, params=params, timeout=self.timeout)
|
|||
|
|
|
|||
|
|
def get_resource_info(self, path: str, fields: Optional[str] = None) -> requests.Response:
|
|||
|
|
"""
|
|||
|
|
GET /v1/disk/resources
|
|||
|
|
Получение метаинформации о ресурсе (файле или папке).
|
|||
|
|
|
|||
|
|
:param path: Путь к ресурсу на Диске
|
|||
|
|
:param fields: Список возвращаемых полей через запятую (для фильтрации ответа)
|
|||
|
|
:return: Объект ответа requests.Response (200 OK при успехе)
|
|||
|
|
"""
|
|||
|
|
url = self.BASE_URL
|
|||
|
|
params = {"path": path}
|
|||
|
|
if fields:
|
|||
|
|
params["fields"] = fields
|
|||
|
|
return self.session.get(url, params=params, timeout=self.timeout)
|
|||
|
|
|
|||
|
|
def publish_resource(self, path: str) -> requests.Response:
|
|||
|
|
"""
|
|||
|
|
POST /v1/disk/resources/publish
|
|||
|
|
Публикация ресурса (создание публичной ссылки).
|
|||
|
|
|
|||
|
|
:param path: Путь к ресурсу, который необходимо опубликовать
|
|||
|
|
:return: Объект ответа requests.Response (200 OK при успехе)
|
|||
|
|
"""
|
|||
|
|
url = f"{self.BASE_URL}/publish"
|
|||
|
|
params = {"path": path}
|
|||
|
|
return self.session.post(url, params=params, timeout=self.timeout)
|
|||
|
|
|
|||
|
|
def unpublish_resource(self, path: str) -> requests.Response:
|
|||
|
|
"""
|
|||
|
|
PUT /v1/disk/resources/unpublish
|
|||
|
|
Закрытие публичного доступа к ресурсу.
|
|||
|
|
|
|||
|
|
:param path: Путь к ресурсу, доступ к которому закрывается
|
|||
|
|
:return: Объект ответа requests.Response (200 OK при успехе)
|
|||
|
|
"""
|
|||
|
|
url = f"{self.BASE_URL}/unpublish"
|
|||
|
|
params = {"path": path}
|
|||
|
|
return self.session.put(url, params=params, timeout=self.timeout)
|
|||
|
|
|
|||
|
|
def delete_resource(self, path: str, permanently: bool = True) -> requests.Response:
|
|||
|
|
"""
|
|||
|
|
DELETE /v1/disk/resources
|
|||
|
|
Удаление файла или папки.
|
|||
|
|
|
|||
|
|
:param path: Путь к удаляемому ресурсу
|
|||
|
|
:param permanently: True — удалить безвозвратно мимо Корзины; False — переместить в Корзину
|
|||
|
|
:return: Объект ответа requests.Response (204 No Content при успешном синхронном удалении)
|
|||
|
|
"""
|
|||
|
|
url = self.BASE_URL
|
|||
|
|
params = {
|
|||
|
|
"path": path,
|
|||
|
|
"permanently": str(permanently).lower()
|
|||
|
|
}
|
|||
|
|
return self.session.delete(url, params=params, timeout=self.timeout)
|
|||
|
|
''',
|
|||
|
|
"tests/__init__.py": "",
|
|||
|
|
"tests/conftest.py": '''"""
|
|||
|
|
Конфигурационный файл Pytest с фикстурами для инициализации клиента и тестовых данных.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import uuid
|
|||
|
|
import pytest
|
|||
|
|
from dotenv import load_dotenv
|
|||
|
|
|
|||
|
|
from api.disk_client import YandexDiskClient
|
|||
|
|
|
|||
|
|
# Загрузка переменных окружения из .env файла
|
|||
|
|
load_dotenv()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture(scope="session")
|
|||
|
|
def auth_token() -> str:
|
|||
|
|
"""
|
|||
|
|
Фикстура получения токена авторизации.
|
|||
|
|
Проверяет наличие токена в переменных окружения. Если токен не задан,
|
|||
|
|
тесты пропускаются с информативным сообщением.
|
|||
|
|
"""
|
|||
|
|
token = os.getenv("YANDEX_DISK_TOKEN")
|
|||
|
|
if not token or token.strip() == "":
|
|||
|
|
pytest.skip(
|
|||
|
|
"YANDEX_DISK_TOKEN не найден в переменных окружения. "
|
|||
|
|
"Задайте его в .env файле или через export YANDEX_DISK_TOKEN=..."
|
|||
|
|
)
|
|||
|
|
return token.strip()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture(scope="session")
|
|||
|
|
def disk_client(auth_token: str) -> YandexDiskClient:
|
|||
|
|
"""
|
|||
|
|
Фикстура создания экземпляра API-клиента на всю тестовую сессию.
|
|||
|
|
"""
|
|||
|
|
return YandexDiskClient(token=auth_token)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture(scope="function")
|
|||
|
|
def temp_folder(disk_client: YandexDiskClient):
|
|||
|
|
"""
|
|||
|
|
Фикстура создания изолированной временной папки для теста.
|
|||
|
|
Гарантирует удаление созданной папки после завершения теста (Teardown).
|
|||
|
|
"""
|
|||
|
|
unique_suffix = uuid.uuid4().hex[:8]
|
|||
|
|
folder_path = f"pytest_temp_{unique_suffix}"
|
|||
|
|
|
|||
|
|
# Setup: создаем папку
|
|||
|
|
create_resp = disk_client.create_folder(folder_path)
|
|||
|
|
assert create_resp.status_code == 201, f"Не удалось подготовить фикстуру папки: {create_resp.text}"
|
|||
|
|
|
|||
|
|
# Передаем управление в тест
|
|||
|
|
yield folder_path
|
|||
|
|
|
|||
|
|
# Teardown: удаляем папку навсегда (мимо корзины)
|
|||
|
|
disk_client.delete_resource(folder_path, permanently=True)
|
|||
|
|
''',
|
|||
|
|
"tests/test_disk_crud.py": '''"""
|
|||
|
|
Набор автотестов для проверки REST API Яндекс Диска.
|
|||
|
|
Покрывает методы:
|
|||
|
|
- PUT: создание папки (создание ресурса)
|
|||
|
|
- GET: получение метаданных ресурса
|
|||
|
|
- POST: публикация ресурса (открытие публичного доступа)
|
|||
|
|
- DELETE: удаление ресурса (очистка)
|
|||
|
|
А также негативные сценарии (дублирование, несуществующий ресурс).
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import uuid
|
|||
|
|
import pytest
|
|||
|
|
from api.disk_client import YandexDiskClient
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.crud
|
|||
|
|
class TestYandexDiskCRUD:
|
|||
|
|
"""Тесты на основные CRUD-операции с ресурсами Яндекс Диска."""
|
|||
|
|
|
|||
|
|
def test_create_folder_put_success(self, disk_client: YandexDiskClient):
|
|||
|
|
"""
|
|||
|
|
Проверка создания папки (PUT-запрос).
|
|||
|
|
Ожидаемый результат: HTTP 201 Created, в ответе присутствует ссылка на созданный ресурс (href).
|
|||
|
|
"""
|
|||
|
|
folder_name = f"dir_create_{uuid.uuid4().hex[:8]}"
|
|||
|
|
try:
|
|||
|
|
response = disk_client.create_folder(folder_name)
|
|||
|
|
assert response.status_code == 201, f"Ожидался 201, получено {response.status_code}: {response.text}"
|
|||
|
|
|
|||
|
|
data = response.json()
|
|||
|
|
assert "href" in data, "В теле ответа отсутствует поле 'href'"
|
|||
|
|
assert "method" in data, "В теле ответа отсутствует поле 'method'"
|
|||
|
|
assert data["method"] == "GET"
|
|||
|
|
finally:
|
|||
|
|
# Очистка ресурса после теста
|
|||
|
|
disk_client.delete_resource(folder_name, permanently=True)
|
|||
|
|
|
|||
|
|
def test_get_resource_info_get_success(self, disk_client: YandexDiskClient, temp_folder: str):
|
|||
|
|
"""
|
|||
|
|
Проверка получения метаданных ресурса (GET-запрос).
|
|||
|
|
Ожидаемый результат: HTTP 200 OK, тип ресурса 'dir', имя совпадает с созданным.
|
|||
|
|
"""
|
|||
|
|
response = disk_client.get_resource_info(temp_folder)
|
|||
|
|
assert response.status_code == 200, f"Ожидался 200, получено {response.status_code}: {response.text}"
|
|||
|
|
|
|||
|
|
data = response.json()
|
|||
|
|
assert data.get("type") == "dir", f"Ожидался тип 'dir', получено {data.get('type')}"
|
|||
|
|
assert data.get("name") == temp_folder, f"Имя ресурса {data.get('name')} не совпадает с {temp_folder}"
|
|||
|
|
assert "created" in data, "Поле 'created' отсутствует в метаданных"
|
|||
|
|
|
|||
|
|
def test_publish_resource_post_success(self, disk_client: YandexDiskClient, temp_folder: str):
|
|||
|
|
"""
|
|||
|
|
Проверка публикации ресурса (POST-запрос).
|
|||
|
|
Ожидаемый результат: HTTP 200 OK, у папки появляется публичный URL и ключ.
|
|||
|
|
"""
|
|||
|
|
response = disk_client.publish_resource(temp_folder)
|
|||
|
|
assert response.status_code == 200, f"Ожидался 200, получено {response.status_code}: {response.text}"
|
|||
|
|
|
|||
|
|
# Проверяем метаданные папки после публикации через GET
|
|||
|
|
info_resp = disk_client.get_resource_info(temp_folder)
|
|||
|
|
assert info_resp.status_code == 200
|
|||
|
|
info_data = info_resp.json()
|
|||
|
|
|
|||
|
|
assert "public_url" in info_data, "После публикации поле 'public_url' отсутствует"
|
|||
|
|
assert "public_key" in info_data, "После публикации поле 'public_key' отсутствует"
|
|||
|
|
assert info_data["public_url"].startswith("https://"), "Публичный URL имеет некорректный формат"
|
|||
|
|
|
|||
|
|
def test_delete_folder_delete_success(self, disk_client: YandexDiskClient):
|
|||
|
|
"""
|
|||
|
|
Проверка удаления ресурса (DELETE-запрос).
|
|||
|
|
Ожидаемый результат: HTTP 204 No Content при удалении, последующий GET возвращает 404 Not Found.
|
|||
|
|
"""
|
|||
|
|
folder_name = f"dir_delete_{uuid.uuid4().hex[:8]}"
|
|||
|
|
|
|||
|
|
# Предварительно создаем папку для последующего удаления
|
|||
|
|
create_resp = disk_client.create_folder(folder_name)
|
|||
|
|
assert create_resp.status_code == 201
|
|||
|
|
|
|||
|
|
# Удаление
|
|||
|
|
delete_resp = disk_client.delete_resource(folder_name, permanently=True)
|
|||
|
|
assert delete_resp.status_code == 204, f"Ожидался 204, получено {delete_resp.status_code}: {delete_resp.text}"
|
|||
|
|
|
|||
|
|
# Проверка отсутствия ресурса
|
|||
|
|
get_resp = disk_client.get_resource_info(folder_name)
|
|||
|
|
assert get_resp.status_code == 404, f"Ресурс все еще существует, статус {get_resp.status_code}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.negative
|
|||
|
|
class TestYandexDiskNegative:
|
|||
|
|
"""Негативные тесты для проверки корректности обработки ошибок API."""
|
|||
|
|
|
|||
|
|
def test_create_duplicate_folder_put_conflict(self, disk_client: YandexDiskClient, temp_folder: str):
|
|||
|
|
"""
|
|||
|
|
Повторное создание уже существующей папки.
|
|||
|
|
Ожидаемый результат: HTTP 409 Conflict.
|
|||
|
|
"""
|
|||
|
|
response = disk_client.create_folder(temp_folder)
|
|||
|
|
assert response.status_code == 409, f"Ожидался 409 Conflict, получено {response.status_code}"
|
|||
|
|
data = response.json()
|
|||
|
|
assert data.get("error") == "PlatformResourceAlreadyExists", f"Неожиданный код ошибки: {data.get('error')}"
|
|||
|
|
|
|||
|
|
def test_get_non_existent_resource_get_not_found(self, disk_client: YandexDiskClient):
|
|||
|
|
"""
|
|||
|
|
Запрос метаданных несуществующего ресурса.
|
|||
|
|
Ожидаемый результат: HTTP 404 Not Found.
|
|||
|
|
"""
|
|||
|
|
non_existent_path = f"does_not_exist_{uuid.uuid4().hex}"
|
|||
|
|
response = disk_client.get_resource_info(non_existent_path)
|
|||
|
|
assert response.status_code == 404, f"Ожидался 404 Not Found, получено {response.status_code}"
|
|||
|
|
data = response.json()
|
|||
|
|
assert data.get("error") == "DiskNotFoundError", f"Неожиданный код ошибки: {data.get('error')}"
|
|||
|
|
|
|||
|
|
def test_unauthorized_access(self):
|
|||
|
|
"""
|
|||
|
|
Выполнение запроса с недействительным токеном.
|
|||
|
|
Ожидаемый результат: HTTP 401 Unauthorized.
|
|||
|
|
"""
|
|||
|
|
invalid_client = YandexDiskClient(token="invalid_dummy_token_123")
|
|||
|
|
response = invalid_client.get_resource_info("any_folder")
|
|||
|
|
assert response.status_code == 401, f"Ожидался 401 Unauthorized, получено {response.status_code}"
|
|||
|
|
data = response.json()
|
|||
|
|
assert data.get("error") == "UnauthorizedError", f"Неожиданный код ошибки: {data.get('error')}"
|
|||
|
|
''',
|
|||
|
|
".github/workflows/tests.yml": """name: Run Yandex Disk API Tests
|
|||
|
|
|
|||
|
|
on:
|
|||
|
|
push:
|
|||
|
|
branches: [ main, master ]
|
|||
|
|
pull_request:
|
|||
|
|
branches: [ main, master ]
|
|||
|
|
workflow_dispatch:
|
|||
|
|
|
|||
|
|
jobs:
|
|||
|
|
test:
|
|||
|
|
runs-on: ubuntu-latest
|
|||
|
|
|
|||
|
|
steps:
|
|||
|
|
- name: Checkout code
|
|||
|
|
uses: actions/checkout@v4
|
|||
|
|
|
|||
|
|
- name: Set up Python 3.11
|
|||
|
|
uses: actions/setup-python@v5
|
|||
|
|
with:
|
|||
|
|
python-version: "3.11"
|
|||
|
|
cache: "pip"
|
|||
|
|
|
|||
|
|
- name: Install dependencies
|
|||
|
|
run: |
|
|||
|
|
python -m pip install --upgrade pip
|
|||
|
|
pip install -r requirements.txt
|
|||
|
|
|
|||
|
|
- name: Run pytest
|
|||
|
|
env:
|
|||
|
|
YANDEX_DISK_TOKEN: ${{ secrets.YANDEX_DISK_TOKEN }}
|
|||
|
|
run: |
|
|||
|
|
pytest -v
|
|||
|
|
""",
|
|||
|
|
"README.md": """# Yandex Disk REST API Automation Tests
|
|||
|
|
|
|||
|
|
Проект автоматизированного тестирования сервиса **Яндекс Диск** на базе **Python**, **pytest** и **requests**.
|
|||
|
|
|
|||
|
|
Проект разработан в соответствии с техническим заданием и проверяет работу ключевых методов REST API:
|
|||
|
|
- **PUT** — создание ресурса (папки)
|
|||
|
|
- **GET** — чтение метаданных ресурса
|
|||
|
|
- **POST** — публикация ресурса (получение публичной ссылки)
|
|||
|
|
- **DELETE** — удаление ресурса (очистка диска)
|
|||
|
|
|
|||
|
|
Также включены негативные сценарии (попытка создания дубликата, обращение к несуществующему ресурсу, невалидный токен).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🛠 Стек технологий
|
|||
|
|
- **Python 3.10+**
|
|||
|
|
- **Pytest** — тестовый фреймворк, фикстуры, параметризация
|
|||
|
|
- **Requests** — HTTP-клиент
|
|||
|
|
- **Python-dotenv** — управление секретами и переменными окружения
|
|||
|
|
- **GitHub Actions** — CI/CD пайплайн для автоматического прогона тестов
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📁 Структура проекта
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
yandex-disk-api-tests/
|
|||
|
|
├── .github/
|
|||
|
|
│ └── workflows/
|
|||
|
|
│ └── tests.yml # CI/CD пайплайн (GitHub Actions)
|
|||
|
|
├── api/
|
|||
|
|
│ ├── __init__.py
|
|||
|
|
│ └── disk_client.py # API-клиент (обёртка над requests с методами GET, POST, PUT, DELETE)
|
|||
|
|
├── tests/
|
|||
|
|
│ ├── __init__.py
|
|||
|
|
│ ├── conftest.py # Сессионные фикстуры и teardown временных ресурсов
|
|||
|
|
│ └── test_disk_crud.py # Позитивные и негативные тестовые сценарии
|
|||
|
|
├── .env.example # Шаблон переменных окружения
|
|||
|
|
├── .gitignore # Исключения для git
|
|||
|
|
├── pytest.ini # Конфигурация запуска Pytest
|
|||
|
|
├── requirements.txt # Список зависимостей
|
|||
|
|
└── README.md # Документация проекта
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔑 Подготовка и авторизация
|
|||
|
|
|
|||
|
|
По условиям задания запрещено использовать личную учетную запись для получения OAuth-токена.
|
|||
|
|
|
|||
|
|
1. Зарегистрируйте отдельный тестовый аккаунт на Яндексе или воспользуйтесь [Полигоном API Яндекс Диска](https://yandex.ru/dev/disk/poligon/).
|
|||
|
|
2. Получите OAuth-токен с правами доступа к Яндекс Диску (`cloud_api:disk.read`, `cloud_api:disk.write` или полный доступ к Диску).
|
|||
|
|
3. Скопируйте полученный токен.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🚀 Локальный запуск
|
|||
|
|
|
|||
|
|
### 1. Клонирование репозитория
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git clone [https://github.com/](https://github.com/)<your-username>/yandex-disk-api-tests.git
|
|||
|
|
cd yandex-disk-api-tests
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Создание и активация виртуального окружения
|
|||
|
|
|
|||
|
|
* **Linux / macOS:**
|
|||
|
|
```bash
|
|||
|
|
python3 -m venv .venv
|
|||
|
|
source .venv/bin/activate
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
|
|||
|
|
* **Windows (PowerShell):**
|
|||
|
|
```powershell
|
|||
|
|
python -m venv .venv
|
|||
|
|
.venv\\Scripts\\Activate.ps1
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
### 3. Установка зависимостей
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
pip install --upgrade pip
|
|||
|
|
pip install -r requirements.txt
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. Настройка переменных окружения
|
|||
|
|
|
|||
|
|
Создайте файл `.env` на основе примера `.env.example`:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cp .env.example .env
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Откройте файл `.env` и укажите ваш токен:
|
|||
|
|
|
|||
|
|
```ini
|
|||
|
|
YANDEX_DISK_TOKEN=ваш_oauth_токен_здесь
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5. Запуск тестов
|
|||
|
|
|
|||
|
|
* Запуск всех тестов с подробным выводом:
|
|||
|
|
```bash
|
|||
|
|
pytest
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
|
|||
|
|
* Запуск только CRUD-тестов:
|
|||
|
|
```bash
|
|||
|
|
pytest -m crud
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
|
|||
|
|
* Запуск только негативных тестов:
|
|||
|
|
```bash
|
|||
|
|
pytest -m negative
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ⚙️ Настройка CI/CD (GitHub Actions)
|
|||
|
|
|
|||
|
|
В репозитории настроен автоматический запуск тестов при каждом пуше и Pull Request в ветки `main` / `master`.
|
|||
|
|
|
|||
|
|
Чтобы тесты выполнялись в GitHub Actions:
|
|||
|
|
|
|||
|
|
1. Перейдите в ваш репозиторий на GitHub: **Settings** -> **Secrets and variables** -> **Actions**.
|
|||
|
|
2. Нажмите кнопку **New repository secret**.
|
|||
|
|
3. Добавьте секрет:
|
|||
|
|
* **Name:** `YANDEX_DISK_TOKEN`
|
|||
|
|
* **Value:** `<ваш_oauth_токен>`
|
|||
|
|
|
|||
|
|
|
|||
|
|
4. После сохранения секрета тесты будут автоматически запускаться во вкладке **Actions**.
|
|||
|
|
"""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Write files locally and create a zip archive
|
|||
|
|
|
|||
|
|
zip_filename = "yandex-disk-api-tests.zip"
|
|||
|
|
with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
|||
|
|
for file_path, content in project_structure.items():
|
|||
|
|
dir_name = os.path.dirname(file_path)
|
|||
|
|
if dir_name:
|
|||
|
|
os.makedirs(dir_name, exist_ok=True)
|
|||
|
|
with open(file_path, "w", encoding="utf-8") as f:
|
|||
|
|
f.write(content)
|
|||
|
|
zip_file.write(file_path)
|
|||
|
|
|
|||
|
|
print(f"Successfully generated {zip_filename} with {len(project_structure)} files.")
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```text?code_stdout&code_event_index=1
|
|||
|
|
Successfully generated yandex-disk-api-tests.zip with 11 files.
|
|||
|
|
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Готовый архив с проектом автотестов подготовлен:
|
|||
|
|
|
|||
|
|
Ниже представлены исходные файлы, конфигурация и документация для создания и публикации репозитория на GitHub.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Структура репозитория
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
yandex-disk-api-tests/
|
|||
|
|
├── .github/
|
|||
|
|
│ └── workflows/
|
|||
|
|
│ └── tests.yml # CI/CD пайплайн для GitHub Actions
|
|||
|
|
├── api/
|
|||
|
|
│ ├── __init__.py # Маркер пакета Python
|
|||
|
|
│ └── disk_client.py # HTTP-клиент (API Wrapper для Яндекс Диска)
|
|||
|
|
├── tests/
|
|||
|
|
│ ├── __init__.py # Маркер пакета Python
|
|||
|
|
│ ├── conftest.py # Сессионные фикстуры и teardown временных ресурсов
|
|||
|
|
│ └── test_disk_crud.py # Тестовые сценарии (GET, POST, PUT, DELETE + негативные)
|
|||
|
|
├── .env.example # Пример файла с переменными окружения
|
|||
|
|
├── .gitignore # Правила исключения временных файлов и секретов
|
|||
|
|
├── pytest.ini # Глобальные параметры запуска Pytest
|
|||
|
|
├── requirements.txt # Зависимости проекта
|
|||
|
|
└── README.md # Описание проекта и инструкция по запуску
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 1. Зависимости и конфигурация
|
|||
|
|
|
|||
|
|
#### `requirements.txt`
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
pytest>=8.0.0
|
|||
|
|
requests>=2.31.0
|
|||
|
|
python-dotenv>=1.0.0
|
|||
|
|
allure-pytest>=2.13.0
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### `.gitignore`
|
|||
|
|
|
|||
|
|
```gitignore
|
|||
|
|
# Секреты и локальные переменные окружения
|
|||
|
|
.env
|
|||
|
|
.env.local
|
|||
|
|
*.env
|
|||
|
|
|
|||
|
|
# Виртуальные окружения Python
|
|||
|
|
venv/
|
|||
|
|
.venv/
|
|||
|
|
env/
|
|||
|
|
ENV/
|
|||
|
|
|
|||
|
|
# Байткод и кэш Python
|
|||
|
|
__pycache__/
|
|||
|
|
*.py[cod]
|
|||
|
|
*$py.class
|
|||
|
|
*.so
|
|||
|
|
build/
|
|||
|
|
develop-eggs/
|
|||
|
|
dist/
|
|||
|
|
downloads/
|
|||
|
|
eggs/
|
|||
|
|
.eggs/
|
|||
|
|
lib/
|
|||
|
|
lib64/
|
|||
|
|
parts/
|
|||
|
|
sdist/
|
|||
|
|
var/
|
|||
|
|
wheels/
|
|||
|
|
*.egg-info/
|
|||
|
|
.installed.cfg
|
|||
|
|
*.egg
|
|||
|
|
|
|||
|
|
# Конфигурации IDE и редакторов
|
|||
|
|
.vscode/
|
|||
|
|
.idea/
|
|||
|
|
*.swp
|
|||
|
|
*.swo
|
|||
|
|
|
|||
|
|
# Артефакты тестов и отчетов
|
|||
|
|
.pytest_cache/
|
|||
|
|
.coverage
|
|||
|
|
htmlcov/
|
|||
|
|
allure-results/
|
|||
|
|
allure-report/
|
|||
|
|
.cache/
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### `.env.example`
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# OAuth-токен от тестовой учетной записи Яндекс Диска / Полигона API
|
|||
|
|
# Получить токен: https://yandex.ru/dev/disk/poligon/
|
|||
|
|
YANDEX_DISK_TOKEN=y0_AgAAAA...
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### `pytest.ini`
|
|||
|
|
|
|||
|
|
```ini
|
|||
|
|
[pytest]
|
|||
|
|
testpaths = tests
|
|||
|
|
python_files = test_*.py
|
|||
|
|
python_classes = Test*
|
|||
|
|
python_functions = test_*
|
|||
|
|
addopts = -v -s --tb=short
|
|||
|
|
markers =
|
|||
|
|
crud: основные CRUD-сценарии API (GET, POST, PUT, DELETE)
|
|||
|
|
negative: негативные тесты на обработку ошибок
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. Исходный код клиента и тестов
|
|||
|
|
|
|||
|
|
#### `api/__init__.py`
|
|||
|
|
|
|||
|
|
*(Пустой файл)*
|
|||
|
|
|
|||
|
|
#### `api/disk_client.py`
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
"""
|
|||
|
|
Клиент для взаимодействия с REST API Яндекс Диска v1.
|
|||
|
|
Официальная документация: https://yandex.ru/dev/disk/api/concepts/about.html
|
|||
|
|
Полигон API: https://cloud-api.yandex.net
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from typing import Optional
|
|||
|
|
import requests
|
|||
|
|
|
|||
|
|
|
|||
|
|
class YandexDiskClient:
|
|||
|
|
"""
|
|||
|
|
HTTP-клиент-обёртка для отправки запросов к API Яндекс Диска.
|
|||
|
|
Инкапсулирует методы GET, POST, PUT, DELETE, управление авторизацией и таймаутами.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
BASE_URL = "https://cloud-api.yandex.net/v1/disk/resources"
|
|||
|
|
|
|||
|
|
def __init__(self, token: str, timeout: int = 10):
|
|||
|
|
"""
|
|||
|
|
Инициализирует клиент с преднастроенной сессией и OAuth-заголовком.
|
|||
|
|
|
|||
|
|
:param token: OAuth-токен Яндекс Диска.
|
|||
|
|
:param timeout: Максимальное время ожидания ответа сервиса (в секундах).
|
|||
|
|
"""
|
|||
|
|
self.timeout = timeout
|
|||
|
|
self.session = requests.Session()
|
|||
|
|
|
|||
|
|
# Яндекс Диск требует передавать токен в формате "OAuth <токен>"
|
|||
|
|
self.session.headers.update({
|
|||
|
|
"Authorization": f"OAuth {token}",
|
|||
|
|
"Accept": "application/json",
|
|||
|
|
"Content-Type": "application/json"
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
def create_folder(self, path: str) -> requests.Response:
|
|||
|
|
"""
|
|||
|
|
PUT /v1/disk/resources
|
|||
|
|
Создает новую папку на Яндекс Диске.
|
|||
|
|
|
|||
|
|
:param path: Путь к создаваемой папке (например, 'test_folder').
|
|||
|
|
:return: requests.Response (201 Created при успехе).
|
|||
|
|
"""
|
|||
|
|
params = {"path": path}
|
|||
|
|
return self.session.put(self.BASE_URL, params=params, timeout=self.timeout)
|
|||
|
|
|
|||
|
|
def get_resource_info(self, path: str, fields: Optional[str] = None) -> requests.Response:
|
|||
|
|
"""
|
|||
|
|
GET /v1/disk/resources
|
|||
|
|
Возвращает метаданные файла или папки.
|
|||
|
|
|
|||
|
|
:param path: Путь к запрашиваемому ресурсу.
|
|||
|
|
:param fields: Список запрашиваемых полей через запятую (для фильтрации).
|
|||
|
|
:return: requests.Response (200 OK при успехе).
|
|||
|
|
"""
|
|||
|
|
params = {"path": path}
|
|||
|
|
if fields:
|
|||
|
|
params["fields"] = fields
|
|||
|
|
return self.session.get(self.BASE_URL, params=params, timeout=self.timeout)
|
|||
|
|
|
|||
|
|
def publish_resource(self, path: str) -> requests.Response:
|
|||
|
|
"""
|
|||
|
|
POST /v1/disk/resources/publish
|
|||
|
|
Публикует ресурс (делает доступным по публичной ссылке).
|
|||
|
|
|
|||
|
|
:param path: Путь к публикуемому ресурсу.
|
|||
|
|
:return: requests.Response (200 OK при успехе).
|
|||
|
|
"""
|
|||
|
|
url = f"{self.BASE_URL}/publish"
|
|||
|
|
params = {"path": path}
|
|||
|
|
return self.session.post(url, params=params, timeout=self.timeout)
|
|||
|
|
|
|||
|
|
def unpublish_resource(self, path: str) -> requests.Response:
|
|||
|
|
"""
|
|||
|
|
PUT /v1/disk/resources/unpublish
|
|||
|
|
Закрывает публичный доступ к ранее опубликованному ресурсу.
|
|||
|
|
|
|||
|
|
:param path: Путь к ресурсу.
|
|||
|
|
:return: requests.Response (200 OK при успехе).
|
|||
|
|
"""
|
|||
|
|
url = f"{self.BASE_URL}/unpublish"
|
|||
|
|
params = {"path": path}
|
|||
|
|
return self.session.put(url, params=params, timeout=self.timeout)
|
|||
|
|
|
|||
|
|
def delete_resource(self, path: str, permanently: bool = True) -> requests.Response:
|
|||
|
|
"""
|
|||
|
|
DELETE /v1/disk/resources
|
|||
|
|
Удаляет файл или папку.
|
|||
|
|
|
|||
|
|
:param path: Путь к удаляемому ресурсу.
|
|||
|
|
:param permanently: True — удалить безвозвратно мимо Корзины; False — отправить в Корзину.
|
|||
|
|
:return: requests.Response (204 No Content при синхронном удалении).
|
|||
|
|
"""
|
|||
|
|
params = {
|
|||
|
|
"path": path,
|
|||
|
|
"permanently": str(permanently).lower()
|
|||
|
|
}
|
|||
|
|
return self.session.delete(self.BASE_URL, params=params, timeout=self.timeout)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### `tests/__init__.py`
|
|||
|
|
|
|||
|
|
*(Пустой файл)*
|
|||
|
|
|
|||
|
|
#### `tests/conftest.py`
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
"""
|
|||
|
|
Конфигурация фикстур pytest для интеграционного тестирования.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import uuid
|
|||
|
|
import pytest
|
|||
|
|
from dotenv import load_dotenv
|
|||
|
|
|
|||
|
|
from api.disk_client import YandexDiskClient
|
|||
|
|
|
|||
|
|
# Загружаем переменные окружения из файла .env (если он существует)
|
|||
|
|
load_dotenv()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture(scope="session")
|
|||
|
|
def auth_token() -> str:
|
|||
|
|
"""
|
|||
|
|
Сессионная фикстура получения OAuth-токена.
|
|||
|
|
Если токен не передан, прерывает прогон тестов с понятным сообщением.
|
|||
|
|
"""
|
|||
|
|
token = os.getenv("YANDEX_DISK_TOKEN")
|
|||
|
|
if not token or not token.strip():
|
|||
|
|
pytest.skip(
|
|||
|
|
"Пропуск тестов: переменная окружения YANDEX_DISK_TOKEN не задана. "
|
|||
|
|
"Задайте её в файле .env или через экспорт окружения."
|
|||
|
|
)
|
|||
|
|
return token.strip()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture(scope="session")
|
|||
|
|
def disk_client(auth_token: str) -> YandexDiskClient:
|
|||
|
|
"""
|
|||
|
|
Создает один общий экземпляр YandexDiskClient на всю тестовую сессию.
|
|||
|
|
"""
|
|||
|
|
return YandexDiskClient(token=auth_token)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture(scope="function")
|
|||
|
|
def temp_folder(disk_client: YandexDiskClient):
|
|||
|
|
"""
|
|||
|
|
Фикстура изолированной папки для теста с автоматической очисткой (Teardown).
|
|||
|
|
Создает директорию перед тестом и гарантированно удаляет её после.
|
|||
|
|
"""
|
|||
|
|
folder_name = f"pytest_fixture_{uuid.uuid4().hex[:8]}"
|
|||
|
|
|
|||
|
|
# SETUP: Создание папки
|
|||
|
|
create_resp = disk_client.create_folder(folder_name)
|
|||
|
|
assert create_resp.status_code == 201, (
|
|||
|
|
f"Ошибка инициализации фикстуры папки: {create_resp.status_code}, {create_resp.text}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Передача управления в тест
|
|||
|
|
yield folder_name
|
|||
|
|
|
|||
|
|
# TEARDOWN: Гарантированная очистка созданного ресурса
|
|||
|
|
disk_client.delete_resource(folder_name, permanently=True)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### `tests/test_disk_crud.py`
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
"""
|
|||
|
|
Набор автоматических тестов для проверки REST API Яндекс Диска.
|
|||
|
|
|
|||
|
|
Покрывает:
|
|||
|
|
- PUT: создание новой папки;
|
|||
|
|
- GET: получение метаданных ресурса;
|
|||
|
|
- POST: публикация ресурса и генерация публичной ссылки;
|
|||
|
|
- DELETE: удаление ресурса мимо корзины;
|
|||
|
|
- Негативные сценарии: дублирование, 404, неверный токен.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import uuid
|
|||
|
|
import pytest
|
|||
|
|
from api.disk_client import YandexDiskClient
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.crud
|
|||
|
|
class TestYandexDiskCRUD:
|
|||
|
|
"""Тестирование успешных CRUD операций."""
|
|||
|
|
|
|||
|
|
def test_create_folder_put_success(self, disk_client: YandexDiskClient):
|
|||
|
|
"""
|
|||
|
|
Проверка метода PUT: создание новой папки.
|
|||
|
|
Ожидаемый статус: 201 Created. В теле ответа присутствует поле 'href'.
|
|||
|
|
"""
|
|||
|
|
folder_name = f"dir_create_{uuid.uuid4().hex[:8]}"
|
|||
|
|
try:
|
|||
|
|
response = disk_client.create_folder(folder_name)
|
|||
|
|
assert response.status_code == 201, f"Ожидался 201, получен {response.status_code}: {response.text}"
|
|||
|
|
|
|||
|
|
data = response.json()
|
|||
|
|
assert "href" in data, "Ответ не содержит обязательное поле 'href'"
|
|||
|
|
assert data.get("method") == "GET", "Метод перехода по ссылке должен быть GET"
|
|||
|
|
finally:
|
|||
|
|
# Очищаем ресурс даже в случае падения ассерта
|
|||
|
|
disk_client.delete_resource(folder_name, permanently=True)
|
|||
|
|
|
|||
|
|
def test_get_resource_info_get_success(self, disk_client: YandexDiskClient, temp_folder: str):
|
|||
|
|
"""
|
|||
|
|
Проверка метода GET: чтение метаданных существующей папки.
|
|||
|
|
Ожидаемый статус: 200 OK. Тип ресурса — 'dir', имя совпадает с запрошенным.
|
|||
|
|
"""
|
|||
|
|
response = disk_client.get_resource_info(temp_folder)
|
|||
|
|
assert response.status_code == 200, f"Ожидался 200, получен {response.status_code}: {response.text}"
|
|||
|
|
|
|||
|
|
data = response.json()
|
|||
|
|
assert data.get("type") == "dir", f"Ожидался тип 'dir', получен {data.get('type')}"
|
|||
|
|
assert data.get("name") == temp_folder, f"Имя ресурса не совпадает: {data.get('name')} != {temp_folder}"
|
|||
|
|
assert "created" in data, "Метаданные должны содержать дату создания ('created')"
|
|||
|
|
|
|||
|
|
def test_publish_resource_post_success(self, disk_client: YandexDiskClient, temp_folder: str):
|
|||
|
|
"""
|
|||
|
|
Проверка метода POST: публикация ресурса (открытие публичного доступа).
|
|||
|
|
Ожидаемый статус: 200 OK. У ресурса появляется публичный URL.
|
|||
|
|
"""
|
|||
|
|
response = disk_client.publish_resource(temp_folder)
|
|||
|
|
assert response.status_code == 200, f"Ожидался 200, получен {response.status_code}: {response.text}"
|
|||
|
|
|
|||
|
|
# Запрашиваем актуальные метаданные через GET для верификации публикации
|
|||
|
|
info_resp = disk_client.get_resource_info(temp_folder)
|
|||
|
|
assert info_resp.status_code == 200
|
|||
|
|
info_data = info_resp.json()
|
|||
|
|
|
|||
|
|
assert "public_url" in info_data, "После публикации отсутствует ключ 'public_url'"
|
|||
|
|
assert info_data["public_url"].startswith("https://"), "Некорректный формат публичной ссылки"
|
|||
|
|
|
|||
|
|
def test_delete_folder_delete_success(self, disk_client: YandexDiskClient):
|
|||
|
|
"""
|
|||
|
|
Проверка метода DELETE: удаление ресурса.
|
|||
|
|
Ожидаемый статус: 204 No Content. Последующий GET-запрос должен возвращать 404 Not Found.
|
|||
|
|
"""
|
|||
|
|
folder_name = f"dir_delete_{uuid.uuid4().hex[:8]}"
|
|||
|
|
|
|||
|
|
# Шаг 1: Создаем ресурс
|
|||
|
|
create_resp = disk_client.create_folder(folder_name)
|
|||
|
|
assert create_resp.status_code == 201
|
|||
|
|
|
|||
|
|
# Шаг 2: Удаляем созданный ресурс
|
|||
|
|
delete_resp = disk_client.delete_resource(folder_name, permanently=True)
|
|||
|
|
assert delete_resp.status_code == 204, f"Ожидался 204, получен {delete_resp.status_code}: {delete_resp.text}"
|
|||
|
|
|
|||
|
|
# Шаг 3: Проверяем, что ресурс действительно отсутствует
|
|||
|
|
get_resp = disk_client.get_resource_info(folder_name)
|
|||
|
|
assert get_resp.status_code == 404, f"Ресурс не удален: получен статус {get_resp.status_code}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.negative
|
|||
|
|
class TestYandexDiskNegative:
|
|||
|
|
"""Негативные сценарии для проверки валидации ошибок API."""
|
|||
|
|
|
|||
|
|
def test_create_duplicate_folder_conflict(self, disk_client: YandexDiskClient, temp_folder: str):
|
|||
|
|
"""
|
|||
|
|
Повторное создание уже существующей папки.
|
|||
|
|
Ожидаемый статус: 409 Conflict.
|
|||
|
|
"""
|
|||
|
|
response = disk_client.create_folder(temp_folder)
|
|||
|
|
assert response.status_code == 409, f"Ожидался 409, получен {response.status_code}"
|
|||
|
|
|
|||
|
|
data = response.json()
|
|||
|
|
assert data.get("error") == "PlatformResourceAlreadyExists"
|
|||
|
|
|
|||
|
|
def test_get_non_existent_resource_not_found(self, disk_client: YandexDiskClient):
|
|||
|
|
"""
|
|||
|
|
Запрос метаданных несуществующего ресурса.
|
|||
|
|
Ожидаемый статус: 404 Not Found.
|
|||
|
|
"""
|
|||
|
|
non_existent_path = f"dir_not_exist_{uuid.uuid4().hex}"
|
|||
|
|
response = disk_client.get_resource_info(non_existent_path)
|
|||
|
|
assert response.status_code == 404, f"Ожидался 404, получен {response.status_code}"
|
|||
|
|
|
|||
|
|
data = response.json()
|
|||
|
|
assert data.get("error") == "DiskNotFoundError"
|
|||
|
|
|
|||
|
|
def test_unauthorized_request(self):
|
|||
|
|
"""
|
|||
|
|
Отправка запроса с невалидным токеном авторизации.
|
|||
|
|
Ожидаемый статус: 401 Unauthorized.
|
|||
|
|
"""
|
|||
|
|
unauth_client = YandexDiskClient(token="invalid_dummy_token_999")
|
|||
|
|
response = unauth_client.get_resource_info("any_test_folder")
|
|||
|
|
assert response.status_code == 401, f"Ожидался 401, получен {response.status_code}"
|
|||
|
|
|
|||
|
|
data = response.json()
|
|||
|
|
assert data.get("error") == "UnauthorizedError"
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. CI/CD конфигурация
|
|||
|
|
|
|||
|
|
#### `.github/workflows/tests.yml`
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
name: Run Yandex Disk API Tests
|
|||
|
|
|
|||
|
|
on:
|
|||
|
|
push:
|
|||
|
|
branches: [ main, master ]
|
|||
|
|
pull_request:
|
|||
|
|
branches: [ main, master ]
|
|||
|
|
workflow_dispatch:
|
|||
|
|
|
|||
|
|
jobs:
|
|||
|
|
test:
|
|||
|
|
name: API Automation Tests
|
|||
|
|
runs-on: ubuntu-latest
|
|||
|
|
|
|||
|
|
steps:
|
|||
|
|
- name: Checkout source code
|
|||
|
|
uses: actions/checkout@v4
|
|||
|
|
|
|||
|
|
- name: Set up Python 3.11
|
|||
|
|
uses: actions/setup-python@v5
|
|||
|
|
with:
|
|||
|
|
python-version: "3.11"
|
|||
|
|
cache: "pip"
|
|||
|
|
|
|||
|
|
- name: Install dependencies
|
|||
|
|
run: |
|
|||
|
|
python -m pip install --upgrade pip
|
|||
|
|
pip install -r requirements.txt
|
|||
|
|
|
|||
|
|
- name: Run Pytest
|
|||
|
|
env:
|
|||
|
|
YANDEX_DISK_TOKEN: ${{ secrets.YANDEX_DISK_TOKEN }}
|
|||
|
|
run: |
|
|||
|
|
pytest -v
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. Оформление для GitHub: `README.md`
|
|||
|
|
|
|||
|
|
```markdown
|
|||
|
|
# Yandex Disk REST API Automation Tests
|
|||
|
|
|
|||
|
|
Проект автоматизированного тестирования сервиса **Яндекс Диск** на базе **Python**, **pytest** и **requests**.
|
|||
|
|
|
|||
|
|
Проект проверяет работу ключевых HTTP-методов REST API:
|
|||
|
|
- **PUT** — создание папки (создание ресурса);
|
|||
|
|
- **GET** — получение метаданных созданного ресурса;
|
|||
|
|
- **POST** — публикация ресурса (открытие публичного доступа);
|
|||
|
|
- **DELETE** — удаление ресурса мимо Корзины;
|
|||
|
|
- **Негативные сценарии** — дублирование ресурса (409), поиск отсутствующего ресурса (404), невалидный токен (401).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🛠 Стек технологий
|
|||
|
|
- **Python 3.10+**
|
|||
|
|
- **pytest** — тестовый фреймворк, фикстуры setup/teardown, маркировка тестов
|
|||
|
|
- **requests** — HTTP-клиент с управлением сессиями
|
|||
|
|
- **python-dotenv** — чтение конфигураций и токенов из `.env`
|
|||
|
|
- **GitHub Actions** — автоматический прогон тестов при пуше/PR
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📁 Структура проекта
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
yandex-disk-api-tests/
|
|||
|
|
├── .github/
|
|||
|
|
│ └── workflows/
|
|||
|
|
│ └── tests.yml # CI/CD пайплайн (GitHub Actions)
|
|||
|
|
├── api/
|
|||
|
|
│ ├── __init__.py
|
|||
|
|
│ └── disk_client.py # Клиент Яндекс Диска (GET, POST, PUT, DELETE)
|
|||
|
|
├── tests/
|
|||
|
|
│ ├── __init__.py
|
|||
|
|
│ ├── conftest.py # Сессионные фикстуры и изоляция тестовых данных
|
|||
|
|
│ └── test_disk_crud.py # Тестовые сценарии
|
|||
|
|
├── .env.example # Пример переменных окружения
|
|||
|
|
├── .gitignore # Исключения версионирования
|
|||
|
|
├── pytest.ini # Настройки запуска pytest
|
|||
|
|
├── requirements.txt # Внешние зависимости
|
|||
|
|
└── README.md # Документация
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔑 Подготовка токена авторизации
|
|||
|
|
|
|||
|
|
> ⚠️ По требованию задания запрещено использовать личную учетную запись для получения OAuth-токена.
|
|||
|
|
|
|||
|
|
1. Войдите в отдельный тестовый аккаунт на Яндексе.
|
|||
|
|
2. Перейдите в [Полигон API Яндекс Диска](https://yandex.ru/dev/disk/poligon/) или создайте приложение в [Яндекс OAuth](https://oauth.yandex.ru/) с правами на Яндекс Диск.
|
|||
|
|
3. Скопируйте полученный OAuth-токен.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🚀 Локальный запуск
|
|||
|
|
|
|||
|
|
### 1. Клонирование репозитория
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git clone [https://github.com/](https://github.com/)<your-username>/yandex-disk-api-tests.git
|
|||
|
|
cd yandex-disk-api-tests
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Развертывание виртуального окружения
|
|||
|
|
|
|||
|
|
* **Linux / macOS:**
|
|||
|
|
```bash
|
|||
|
|
python3 -m venv .venv
|
|||
|
|
source .venv/bin/activate
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
|
|||
|
|
* **Windows (PowerShell):**
|
|||
|
|
```powershell
|
|||
|
|
python -m venv .venv
|
|||
|
|
.venv\Scripts\Activate.ps1
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
### 3. Установка зависимостей
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
pip install --upgrade pip
|
|||
|
|
pip install -r requirements.txt
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. Настройка переменных окружения
|
|||
|
|
|
|||
|
|
Создайте файл `.env` на основе шаблона:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cp .env.example .env
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Укажите ваш OAuth-токен в файле `.env`:
|
|||
|
|
|
|||
|
|
```ini
|
|||
|
|
YANDEX_DISK_TOKEN=ваш_токен_здесь
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5. Запуск автотестов
|
|||
|
|
|
|||
|
|
* Запуск всех тестов:
|
|||
|
|
```bash
|
|||
|
|
pytest
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
|
|||
|
|
* Запуск только CRUD-тестов:
|
|||
|
|
```bash
|
|||
|
|
pytest -m crud
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
|
|||
|
|
* Запуск только негативных тестов:
|
|||
|
|
```bash
|
|||
|
|
pytest -m negative
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ⚙️ Настройка CI/CD (GitHub Actions)
|
|||
|
|
|
|||
|
|
В проекте настроен автоматический запуск тестов при каждом пуше в ветки `main`/`master`.
|
|||
|
|
|
|||
|
|
Для работы тестов в GitHub Actions:
|
|||
|
|
|
|||
|
|
1. Перейдите в репозиторий: **Settings** → **Secrets and variables** → **Actions**.
|
|||
|
|
2. Нажмите **New repository secret**.
|
|||
|
|
3. Задайте имя: `YANDEX_DISK_TOKEN`, значение: `<ваш OAuth-токен>`.
|
|||
|
|
4. Сохраните секрет. Теперь при каждом коммите тесты будут запускаться автоматически.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5. Инструкция по публикации в GitHub
|
|||
|
|
|
|||
|
|
Выполните следующие команды в терминале для отправки проекта в новый публичный репозиторий:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 1. Перейдите в папку проекта
|
|||
|
|
cd yandex-disk-api-tests
|
|||
|
|
|
|||
|
|
# 2. Инициализируйте локальный репозиторий Git
|
|||
|
|
git init
|
|||
|
|
|
|||
|
|
# 3. Добавьте файлы и сделайте первый коммит
|
|||
|
|
git add .
|
|||
|
|
git commit -m "feat: initial commit with Yandex Disk API tests and CI"
|
|||
|
|
|
|||
|
|
# 4. Переименуйте ветку в main
|
|||
|
|
git branch -M main
|
|||
|
|
|
|||
|
|
# 5. Привяжите удаленный репозиторий GitHub (замените URL на свой)
|
|||
|
|
git remote add origin https://github.com/<ваш-логин-github>/yandex-disk-api-tests.git
|
|||
|
|
|
|||
|
|
# 6. Отправьте код на GitHub
|
|||
|
|
git push -u origin main
|
|||
|
|
|
|||
|
|
```
|