diff --git a/doc/doc.md b/doc/doc.md new file mode 100644 index 0000000..edb0048 --- /dev/null +++ b/doc/doc.md @@ -0,0 +1,1215 @@ +```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/)/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/)/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 + +``` \ No newline at end of file