Files
Yandex-disk-API-test/doc/doc.md
T
2026-09-17 21:27:51 +00:00

44 KiB
Raw Blame History


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 Яндекс Диска.
  2. Получите OAuth-токен с правами доступа к Яндекс Диску (cloud_api:disk.read, cloud_api:disk.write или полный доступ к Диску).
  3. Скопируйте полученный токен.

🚀 Локальный запуск

1. Клонирование репозитория

git clone [https://github.com/](https://github.com/)<your-username>/yandex-disk-api-tests.git
cd yandex-disk-api-tests

2. Создание и активация виртуального окружения

  • Linux / macOS:
python3 -m venv .venv
source .venv/bin/activate

  • Windows (PowerShell):
python -m venv .venv
.venv\\Scripts\\Activate.ps1

3. Установка зависимостей

pip install --upgrade pip
pip install -r requirements.txt

4. Настройка переменных окружения

Создайте файл .env на основе примера .env.example:

cp .env.example .env

Откройте файл .env и укажите ваш токен:

YANDEX_DISK_TOKEN=ваш_oauth_токен_здесь

5. Запуск тестов

  • Запуск всех тестов с подробным выводом:
pytest

  • Запуск только CRUD-тестов:
pytest -m crud

  • Запуск только негативных тестов:
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_токен>
  1. После сохранения секрета тесты будут автоматически запускаться во вкладке 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.


Структура репозитория

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

pytest>=8.0.0
requests>=2.31.0
python-dotenv>=1.0.0
allure-pytest>=2.13.0

.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

# OAuth-токен от тестовой учетной записи Яндекс Диска / Полигона API
# Получить токен: https://yandex.ru/dev/disk/poligon/
YANDEX_DISK_TOKEN=y0_AgAAAA...

pytest.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

"""
Клиент для взаимодействия с 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

"""
Конфигурация фикстур 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

"""
Набор автоматических тестов для проверки 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

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

# 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 Яндекс Диска или создайте приложение в Яндекс OAuth с правами на Яндекс Диск.
  3. Скопируйте полученный OAuth-токен.

🚀 Локальный запуск

1. Клонирование репозитория

git clone [https://github.com/](https://github.com/)<your-username>/yandex-disk-api-tests.git
cd yandex-disk-api-tests

2. Развертывание виртуального окружения

  • Linux / macOS:
python3 -m venv .venv
source .venv/bin/activate

  • Windows (PowerShell):
python -m venv .venv
.venv\Scripts\Activate.ps1

3. Установка зависимостей

pip install --upgrade pip
pip install -r requirements.txt

4. Настройка переменных окружения

Создайте файл .env на основе шаблона:

cp .env.example .env

Укажите ваш OAuth-токен в файле .env:

YANDEX_DISK_TOKEN=ваш_токен_здесь

5. Запуск автотестов

  • Запуск всех тестов:
pytest

  • Запуск только CRUD-тестов:
pytest -m crud

  • Запуск только негативных тестов:
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