fix: auto-format code and fix linter errors
This commit is contained in:
parent
b67f0e5031
commit
3995d60b6b
@ -5,27 +5,35 @@ Unofficial Python client for Kwork.ru API.
|
||||
|
||||
Example:
|
||||
from kwork_api import KworkClient
|
||||
|
||||
|
||||
# Login with credentials
|
||||
client = await KworkClient.login("username", "password")
|
||||
|
||||
|
||||
# Or restore from token
|
||||
client = KworkClient(token="your_web_auth_token")
|
||||
|
||||
|
||||
# Get catalog
|
||||
catalog = await client.catalog.get_list(page=1)
|
||||
"""
|
||||
|
||||
from .client import KworkClient
|
||||
from .errors import KworkError, KworkAuthError, KworkApiError
|
||||
from .errors import (
|
||||
KworkApiError,
|
||||
KworkAuthError,
|
||||
KworkError,
|
||||
KworkNetworkError,
|
||||
KworkNotFoundError,
|
||||
KworkRateLimitError,
|
||||
KworkValidationError,
|
||||
)
|
||||
from .models import (
|
||||
ValidationResponse,
|
||||
ValidationIssue,
|
||||
CatalogResponse,
|
||||
Kwork,
|
||||
KworkDetails,
|
||||
Project,
|
||||
CatalogResponse,
|
||||
ProjectsResponse,
|
||||
ValidationIssue,
|
||||
ValidationResponse,
|
||||
)
|
||||
|
||||
__version__ = "0.1.0" # Updated by semantic-release
|
||||
@ -34,6 +42,10 @@ __all__ = [
|
||||
"KworkError",
|
||||
"KworkAuthError",
|
||||
"KworkApiError",
|
||||
"KworkNetworkError",
|
||||
"KworkNotFoundError",
|
||||
"KworkRateLimitError",
|
||||
"KworkValidationError",
|
||||
"ValidationResponse",
|
||||
"ValidationIssue",
|
||||
"Kwork",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -13,7 +13,7 @@
|
||||
└── KworkNetworkError (ошибки сети)
|
||||
"""
|
||||
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
__all__ = [
|
||||
"KworkError",
|
||||
@ -29,25 +29,25 @@ __all__ = [
|
||||
class KworkError(Exception):
|
||||
"""
|
||||
Базовое исключение для всех ошибок Kwork API.
|
||||
|
||||
|
||||
Все остальные исключения наследуются от этого класса.
|
||||
|
||||
|
||||
Attributes:
|
||||
message: Сообщение об ошибке.
|
||||
response: Оригинальный HTTP response (если есть).
|
||||
|
||||
|
||||
Example:
|
||||
try:
|
||||
await client.catalog.get_list()
|
||||
except KworkError as e:
|
||||
print(f"Ошибка: {e.message}")
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, response: Optional[Any] = None):
|
||||
|
||||
def __init__(self, message: str, response: Any | None = None):
|
||||
self.message = message
|
||||
self.response = response
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"KworkError: {self.message}"
|
||||
|
||||
@ -55,22 +55,22 @@ class KworkError(Exception):
|
||||
class KworkAuthError(KworkError):
|
||||
"""
|
||||
Ошибка аутентификации/авторизации.
|
||||
|
||||
|
||||
Возникает при:
|
||||
- Неверном логине или пароле
|
||||
- Истёкшем или невалидном токене
|
||||
- Отсутствии прав доступа (403)
|
||||
|
||||
|
||||
Example:
|
||||
try:
|
||||
client = await KworkClient.login("user", "wrong_password")
|
||||
except KworkAuthError:
|
||||
print("Неверные учётные данные")
|
||||
"""
|
||||
|
||||
def __init__(self, message: str = "Authentication failed", response: Optional[Any] = None):
|
||||
|
||||
def __init__(self, message: str = "Authentication failed", response: Any | None = None):
|
||||
super().__init__(message, response)
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"KworkAuthError: {self.message}"
|
||||
|
||||
@ -78,28 +78,28 @@ class KworkAuthError(KworkError):
|
||||
class KworkApiError(KworkError):
|
||||
"""
|
||||
Ошибка HTTP запроса к API (4xx, 5xx).
|
||||
|
||||
|
||||
Базовый класс для HTTP ошибок API. Содержит код статуса.
|
||||
|
||||
|
||||
Attributes:
|
||||
status_code: HTTP код ответа (400, 404, 500, etc.)
|
||||
|
||||
|
||||
Example:
|
||||
try:
|
||||
await client.catalog.get_details(999999)
|
||||
except KworkApiError as e:
|
||||
print(f"HTTP {e.status_code}: {e.message}")
|
||||
"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
status_code: Optional[int] = None,
|
||||
response: Optional[Any] = None,
|
||||
status_code: int | None = None,
|
||||
response: Any | None = None,
|
||||
):
|
||||
self.status_code = status_code
|
||||
super().__init__(message, response)
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.status_code:
|
||||
return f"KworkApiError [{self.status_code}]: {self.message}"
|
||||
@ -109,46 +109,46 @@ class KworkApiError(KworkError):
|
||||
class KworkNotFoundError(KworkApiError):
|
||||
"""
|
||||
Ресурс не найден (404).
|
||||
|
||||
|
||||
Возникает при запросе несуществующего кворка,
|
||||
пользователя или другого ресурса.
|
||||
|
||||
|
||||
Example:
|
||||
try:
|
||||
await client.catalog.get_details(999999)
|
||||
except KworkNotFoundError:
|
||||
print("Кворк не найден")
|
||||
"""
|
||||
|
||||
def __init__(self, resource: str, response: Optional[Any] = None):
|
||||
|
||||
def __init__(self, resource: str, response: Any | None = None):
|
||||
super().__init__(f"Resource not found: {resource}", 404, response)
|
||||
|
||||
|
||||
class KworkRateLimitError(KworkApiError):
|
||||
"""
|
||||
Превышен лимит запросов (429).
|
||||
|
||||
|
||||
Возникает при слишком частых запросах к API.
|
||||
Рекомендуется сделать паузу перед повторным запросом.
|
||||
|
||||
|
||||
Attributes:
|
||||
retry_after: Время ожидания в секундах (если указано сервером).
|
||||
|
||||
|
||||
Example:
|
||||
import asyncio
|
||||
|
||||
|
||||
try:
|
||||
await client.catalog.get_list()
|
||||
except KworkRateLimitError as e:
|
||||
wait_time = e.retry_after or 5
|
||||
await asyncio.sleep(wait_time)
|
||||
"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Rate limit exceeded",
|
||||
response: Optional[Any] = None,
|
||||
retry_after: Optional[int] = None,
|
||||
response: Any | None = None,
|
||||
retry_after: int | None = None,
|
||||
):
|
||||
self.retry_after = retry_after
|
||||
super().__init__(message, 429, response)
|
||||
@ -157,12 +157,12 @@ class KworkRateLimitError(KworkApiError):
|
||||
class KworkValidationError(KworkApiError):
|
||||
"""
|
||||
Ошибка валидации (400).
|
||||
|
||||
|
||||
Возникает при некорректных данных запроса.
|
||||
|
||||
|
||||
Attributes:
|
||||
fields: Словарь ошибок по полям {field: [errors]}.
|
||||
|
||||
|
||||
Example:
|
||||
try:
|
||||
await client.catalog.get_list(page=-1)
|
||||
@ -171,16 +171,16 @@ class KworkValidationError(KworkApiError):
|
||||
for field, errors in e.fields.items():
|
||||
print(f"{field}: {errors[0]}")
|
||||
"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Validation failed",
|
||||
fields: Optional[dict[str, list[str]]] = None,
|
||||
response: Optional[Any] = None,
|
||||
fields: dict[str, list[str]] | None = None,
|
||||
response: Any | None = None,
|
||||
):
|
||||
self.fields = fields or {}
|
||||
super().__init__(message, 400, response)
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.fields:
|
||||
field_errors = ", ".join(f"{k}: {v[0]}" for k, v in self.fields.items())
|
||||
@ -191,22 +191,22 @@ class KworkValidationError(KworkApiError):
|
||||
class KworkNetworkError(KworkError):
|
||||
"""
|
||||
Ошибка сети/подключения.
|
||||
|
||||
|
||||
Возникает при:
|
||||
- Отсутствии соединения
|
||||
- Таймауте запроса
|
||||
- Ошибке DNS
|
||||
- Проблемах с SSL
|
||||
|
||||
|
||||
Example:
|
||||
try:
|
||||
await client.catalog.get_list()
|
||||
except KworkNetworkError:
|
||||
print("Проверьте подключение к интернету")
|
||||
"""
|
||||
|
||||
def __init__(self, message: str = "Network error", response: Optional[Any] = None):
|
||||
|
||||
def __init__(self, message: str = "Network error", response: Any | None = None):
|
||||
super().__init__(message, response)
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"KworkNetworkError: {self.message}"
|
||||
|
||||
@ -6,7 +6,7 @@ Pydantic модели для ответов Kwork API.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@ -14,47 +14,49 @@ from pydantic import BaseModel, Field
|
||||
class KworkUser(BaseModel):
|
||||
"""
|
||||
Информация о пользователе Kwork.
|
||||
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID пользователя.
|
||||
username: Имя пользователя (логин).
|
||||
avatar_url: URL аватара или None.
|
||||
is_online: Статус онлайн.
|
||||
rating: Рейтинг пользователя (0-5).
|
||||
|
||||
|
||||
Example:
|
||||
user = KworkUser(id=123, username="seller", rating=4.9)
|
||||
print(f"{user.username}: {user.rating} ★")
|
||||
"""
|
||||
|
||||
id: int
|
||||
username: str
|
||||
avatar_url: Optional[str] = None
|
||||
avatar_url: str | None = None
|
||||
is_online: bool = False
|
||||
rating: Optional[float] = None
|
||||
rating: float | None = None
|
||||
|
||||
|
||||
class KworkCategory(BaseModel):
|
||||
"""
|
||||
Категория кворков.
|
||||
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID категории.
|
||||
name: Название категории.
|
||||
slug: URL-safe идентификатор.
|
||||
parent_id: ID родительской категории для вложенности.
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
slug: str
|
||||
parent_id: Optional[int] = None
|
||||
parent_id: int | None = None
|
||||
|
||||
|
||||
class Kwork(BaseModel):
|
||||
"""
|
||||
Кворк — услуга на Kwork.
|
||||
|
||||
|
||||
Базовая модель кворка с основной информацией.
|
||||
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID кворка.
|
||||
title: Заголовок кворка.
|
||||
@ -69,26 +71,27 @@ class Kwork(BaseModel):
|
||||
created_at: Дата создания.
|
||||
updated_at: Дата последнего обновления.
|
||||
"""
|
||||
|
||||
id: int
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
description: str | None = None
|
||||
price: float
|
||||
currency: str = "RUB"
|
||||
category_id: Optional[int] = None
|
||||
seller: Optional[KworkUser] = None
|
||||
category_id: int | None = None
|
||||
seller: KworkUser | None = None
|
||||
images: list[str] = Field(default_factory=list)
|
||||
rating: Optional[float] = None
|
||||
rating: float | None = None
|
||||
reviews_count: int = 0
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class KworkDetails(Kwork):
|
||||
"""
|
||||
Расширенная информация о кворке.
|
||||
|
||||
|
||||
Наследует все поля Kwork плюс дополнительные детали.
|
||||
|
||||
|
||||
Attributes:
|
||||
full_description: Полное описание услуги.
|
||||
requirements: Требования к заказчику.
|
||||
@ -97,10 +100,11 @@ class KworkDetails(Kwork):
|
||||
features: Список дополнительных опций.
|
||||
faq: Список вопросов и ответов.
|
||||
"""
|
||||
full_description: Optional[str] = None
|
||||
requirements: Optional[str] = None
|
||||
delivery_time: Optional[int] = None
|
||||
revisions: Optional[int] = None
|
||||
|
||||
full_description: str | None = None
|
||||
requirements: str | None = None
|
||||
delivery_time: int | None = None
|
||||
revisions: int | None = None
|
||||
features: list[str] = Field(default_factory=list)
|
||||
faq: list[dict[str, str]] = Field(default_factory=list)
|
||||
|
||||
@ -108,7 +112,7 @@ class KworkDetails(Kwork):
|
||||
class PaginationInfo(BaseModel):
|
||||
"""
|
||||
Информация о пагинации.
|
||||
|
||||
|
||||
Attributes:
|
||||
current_page: Текущая страница (начиная с 1).
|
||||
total_pages: Общее количество страниц.
|
||||
@ -117,6 +121,7 @@ class PaginationInfo(BaseModel):
|
||||
has_next: Есть ли следующая страница.
|
||||
has_prev: Есть ли предыдущая страница.
|
||||
"""
|
||||
|
||||
current_page: int = 1
|
||||
total_pages: int = 1
|
||||
total_items: int = 0
|
||||
@ -128,23 +133,24 @@ class PaginationInfo(BaseModel):
|
||||
class CatalogResponse(BaseModel):
|
||||
"""
|
||||
Ответ API каталога кворков.
|
||||
|
||||
|
||||
Attributes:
|
||||
kworks: Список кворков на странице.
|
||||
pagination: Информация о пагинации.
|
||||
filters: Доступные фильтры.
|
||||
sort_options: Доступные опции сортировки.
|
||||
"""
|
||||
|
||||
kworks: list[Kwork] = Field(default_factory=list)
|
||||
pagination: Optional[PaginationInfo] = None
|
||||
filters: Optional[dict[str, Any]] = None
|
||||
pagination: PaginationInfo | None = None
|
||||
filters: dict[str, Any] | None = None
|
||||
sort_options: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class Project(BaseModel):
|
||||
"""
|
||||
Проект (заказ на бирже фриланса).
|
||||
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID проекта.
|
||||
title: Заголовок проекта.
|
||||
@ -159,16 +165,17 @@ class Project(BaseModel):
|
||||
bids_count: Количество откликов.
|
||||
skills: Требуемые навыки.
|
||||
"""
|
||||
|
||||
id: int
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
budget: Optional[float] = None
|
||||
description: str | None = None
|
||||
budget: float | None = None
|
||||
budget_type: str = "fixed"
|
||||
category_id: Optional[int] = None
|
||||
customer: Optional[KworkUser] = None
|
||||
category_id: int | None = None
|
||||
customer: KworkUser | None = None
|
||||
status: str = "open"
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
bids_count: int = 0
|
||||
skills: list[str] = Field(default_factory=list)
|
||||
|
||||
@ -176,19 +183,20 @@ class Project(BaseModel):
|
||||
class ProjectsResponse(BaseModel):
|
||||
"""
|
||||
Ответ API списка проектов.
|
||||
|
||||
|
||||
Attributes:
|
||||
projects: Список проектов.
|
||||
pagination: Информация о пагинации.
|
||||
"""
|
||||
|
||||
projects: list[Project] = Field(default_factory=list)
|
||||
pagination: Optional[PaginationInfo] = None
|
||||
pagination: PaginationInfo | None = None
|
||||
|
||||
|
||||
class Review(BaseModel):
|
||||
"""
|
||||
Отзыв о кворке или проекте.
|
||||
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID отзыва.
|
||||
rating: Оценка от 1 до 5.
|
||||
@ -197,32 +205,34 @@ class Review(BaseModel):
|
||||
kwork_id: ID кворка (если отзыв о кворке).
|
||||
created_at: Дата создания.
|
||||
"""
|
||||
|
||||
id: int
|
||||
rating: int = Field(ge=1, le=5)
|
||||
comment: Optional[str] = None
|
||||
author: Optional[KworkUser] = None
|
||||
kwork_id: Optional[int] = None
|
||||
created_at: Optional[datetime] = None
|
||||
comment: str | None = None
|
||||
author: KworkUser | None = None
|
||||
kwork_id: int | None = None
|
||||
created_at: datetime | None = None
|
||||
|
||||
|
||||
class ReviewsResponse(BaseModel):
|
||||
"""
|
||||
Ответ API списка отзывов.
|
||||
|
||||
|
||||
Attributes:
|
||||
reviews: Список отзывов.
|
||||
pagination: Информация о пагинации.
|
||||
average_rating: Средний рейтинг.
|
||||
"""
|
||||
|
||||
reviews: list[Review] = Field(default_factory=list)
|
||||
pagination: Optional[PaginationInfo] = None
|
||||
average_rating: Optional[float] = None
|
||||
pagination: PaginationInfo | None = None
|
||||
average_rating: float | None = None
|
||||
|
||||
|
||||
class Notification(BaseModel):
|
||||
"""
|
||||
Уведомление пользователя.
|
||||
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID уведомления.
|
||||
type: Тип уведомления: "message", "order", "system", etc.
|
||||
@ -232,23 +242,25 @@ class Notification(BaseModel):
|
||||
created_at: Дата создания.
|
||||
link: Ссылка для перехода (если есть).
|
||||
"""
|
||||
|
||||
id: int
|
||||
type: str
|
||||
title: str
|
||||
message: str
|
||||
is_read: bool = False
|
||||
created_at: Optional[datetime] = None
|
||||
link: Optional[str] = None
|
||||
created_at: datetime | None = None
|
||||
link: str | None = None
|
||||
|
||||
|
||||
class NotificationsResponse(BaseModel):
|
||||
"""
|
||||
Ответ API списка уведомлений.
|
||||
|
||||
|
||||
Attributes:
|
||||
notifications: Список уведомлений.
|
||||
unread_count: Количество непрочитанных уведомлений.
|
||||
"""
|
||||
|
||||
notifications: list[Notification] = Field(default_factory=list)
|
||||
unread_count: int = 0
|
||||
|
||||
@ -256,7 +268,7 @@ class NotificationsResponse(BaseModel):
|
||||
class Dialog(BaseModel):
|
||||
"""
|
||||
Диалог (чат) с пользователем.
|
||||
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID диалога.
|
||||
participant: Собеседник.
|
||||
@ -264,17 +276,18 @@ class Dialog(BaseModel):
|
||||
unread_count: Количество непрочитанных сообщений.
|
||||
updated_at: Время последнего сообщения.
|
||||
"""
|
||||
|
||||
id: int
|
||||
participant: Optional[KworkUser] = None
|
||||
last_message: Optional[str] = None
|
||||
participant: KworkUser | None = None
|
||||
last_message: str | None = None
|
||||
unread_count: int = 0
|
||||
updated_at: Optional[datetime] = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class AuthResponse(BaseModel):
|
||||
"""
|
||||
Ответ API аутентификации.
|
||||
|
||||
|
||||
Attributes:
|
||||
success: Успешность аутентификации.
|
||||
user_id: ID пользователя.
|
||||
@ -282,80 +295,86 @@ class AuthResponse(BaseModel):
|
||||
web_auth_token: Токен для последующих запросов.
|
||||
message: Сообщение (например, об ошибке).
|
||||
"""
|
||||
|
||||
success: bool
|
||||
user_id: Optional[int] = None
|
||||
username: Optional[str] = None
|
||||
web_auth_token: Optional[str] = None
|
||||
message: Optional[str] = None
|
||||
user_id: int | None = None
|
||||
username: str | None = None
|
||||
web_auth_token: str | None = None
|
||||
message: str | None = None
|
||||
|
||||
|
||||
class ErrorDetail(BaseModel):
|
||||
"""
|
||||
Детали ошибки API.
|
||||
|
||||
|
||||
Attributes:
|
||||
code: Код ошибки.
|
||||
message: Сообщение об ошибке.
|
||||
field: Поле, вызвавшее ошибку (если применимо).
|
||||
"""
|
||||
|
||||
code: str
|
||||
message: str
|
||||
field: Optional[str] = None
|
||||
field: str | None = None
|
||||
|
||||
|
||||
class APIErrorResponse(BaseModel):
|
||||
"""
|
||||
Стандартный ответ API об ошибке.
|
||||
|
||||
|
||||
Attributes:
|
||||
success: Всегда False для ошибок.
|
||||
errors: Список деталей ошибок.
|
||||
message: Общее сообщение об ошибке.
|
||||
"""
|
||||
|
||||
success: bool = False
|
||||
errors: list[ErrorDetail] = Field(default_factory=list)
|
||||
message: Optional[str] = None
|
||||
message: str | None = None
|
||||
|
||||
|
||||
class City(BaseModel):
|
||||
"""
|
||||
Город из справочника.
|
||||
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID города.
|
||||
name: Название города.
|
||||
country_id: ID страны.
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
country_id: Optional[int] = None
|
||||
country_id: int | None = None
|
||||
|
||||
|
||||
class Country(BaseModel):
|
||||
"""
|
||||
Страна из справочника.
|
||||
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID страны.
|
||||
name: Название страны.
|
||||
code: Код страны (ISO).
|
||||
cities: Список городов в стране.
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
code: Optional[str] = None
|
||||
code: str | None = None
|
||||
cities: list[City] = Field(default_factory=list)
|
||||
|
||||
|
||||
class TimeZone(BaseModel):
|
||||
"""
|
||||
Часовой пояс.
|
||||
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID.
|
||||
name: Название пояса.
|
||||
offset: Смещение от UTC (например, "+03:00").
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
offset: str
|
||||
@ -364,7 +383,7 @@ class TimeZone(BaseModel):
|
||||
class Feature(BaseModel):
|
||||
"""
|
||||
Дополнительная функция (feature) для кворка.
|
||||
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID функции.
|
||||
name: Название.
|
||||
@ -372,9 +391,10 @@ class Feature(BaseModel):
|
||||
price: Стоимость в рублях.
|
||||
type: Тип: "extra", "premium", etc.
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
description: str | None = None
|
||||
price: float
|
||||
type: str
|
||||
|
||||
@ -382,40 +402,42 @@ class Feature(BaseModel):
|
||||
class Badge(BaseModel):
|
||||
"""
|
||||
Значок (достижение) пользователя.
|
||||
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID значка.
|
||||
name: Название значка.
|
||||
description: Описание достижения.
|
||||
icon_url: URL иконки значка.
|
||||
"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
icon_url: Optional[str] = None
|
||||
description: str | None = None
|
||||
icon_url: str | None = None
|
||||
|
||||
|
||||
# Generic response wrapper
|
||||
class DataResponse(BaseModel):
|
||||
"""
|
||||
Универсальный ответ API с данными.
|
||||
|
||||
|
||||
Используется как обёртка для различных ответов API.
|
||||
|
||||
|
||||
Attributes:
|
||||
success: Успешность запроса.
|
||||
data: Полезные данные (словарь).
|
||||
message: Дополнительное сообщение.
|
||||
"""
|
||||
|
||||
success: bool = True
|
||||
data: Optional[dict[str, Any]] = None
|
||||
message: Optional[str] = None
|
||||
data: dict[str, Any] | None = None
|
||||
message: str | None = None
|
||||
|
||||
|
||||
class ValidationIssue(BaseModel):
|
||||
"""
|
||||
Проблема, найденная при валидации текста.
|
||||
|
||||
|
||||
Attributes:
|
||||
type: Тип проблемы: "error", "warning", "suggestion".
|
||||
code: Код ошибки (например, "SPELLING", "GRAMMAR", "LENGTH").
|
||||
@ -423,19 +445,20 @@ class ValidationIssue(BaseModel):
|
||||
position: Позиция в тексте (если применимо).
|
||||
suggestion: Предлагаемое исправление (если есть).
|
||||
"""
|
||||
|
||||
type: str = "error"
|
||||
code: str
|
||||
message: str
|
||||
position: Optional[int] = None
|
||||
suggestion: Optional[str] = None
|
||||
position: int | None = None
|
||||
suggestion: str | None = None
|
||||
|
||||
|
||||
class ValidationResponse(BaseModel):
|
||||
"""
|
||||
Ответ API валидации текста.
|
||||
|
||||
|
||||
Используется для эндпоинта /api/validation/checktext.
|
||||
|
||||
|
||||
Attributes:
|
||||
success: Успешность валидации.
|
||||
is_valid: Текст проходит валидацию (нет критических ошибок).
|
||||
@ -443,8 +466,9 @@ class ValidationResponse(BaseModel):
|
||||
score: Оценка качества текста (0-100, если доступна).
|
||||
message: Дополнительное сообщение.
|
||||
"""
|
||||
|
||||
success: bool = True
|
||||
is_valid: bool = True
|
||||
issues: list[ValidationIssue] = Field(default_factory=list)
|
||||
score: Optional[int] = None
|
||||
message: Optional[str] = None
|
||||
score: int | None = None
|
||||
message: str | None = None
|
||||
|
||||
1094
test-results/report.html
Normal file
1094
test-results/report.html
Normal file
File diff suppressed because one or more lines are too long
@ -5,8 +5,9 @@ E2E тесты для Kwork API.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Загружаем .env
|
||||
@ -39,21 +40,17 @@ def slowmo(request):
|
||||
slowmo = request.config.getoption("--slowmo", default=0)
|
||||
if slowmo > 0:
|
||||
import time
|
||||
|
||||
time.sleep(slowmo)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Регистрация маркера e2e."""
|
||||
config.addinivalue_line(
|
||||
"markers", "e2e: mark test as end-to-end (requires credentials)"
|
||||
)
|
||||
config.addinivalue_line("markers", "e2e: mark test as end-to-end (requires credentials)")
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
"""Добавляет опцию --slowmo."""
|
||||
parser.addoption(
|
||||
"--slowmo",
|
||||
type=float,
|
||||
default=0,
|
||||
help="Delay between tests in seconds (for rate limiting)"
|
||||
"--slowmo", type=float, default=0, help="Delay between tests in seconds (for rate limiting)"
|
||||
)
|
||||
|
||||
@ -3,6 +3,7 @@ E2E тесты аутентификации.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from kwork_api import KworkClient
|
||||
from kwork_api.errors import KworkAuthError
|
||||
|
||||
@ -11,10 +12,9 @@ from kwork_api.errors import KworkAuthError
|
||||
async def test_login_success(require_credentials):
|
||||
"""E2E: Успешная аутентификация."""
|
||||
client = await KworkClient.login(
|
||||
username=require_credentials["username"],
|
||||
password=require_credentials["password"]
|
||||
username=require_credentials["username"], password=require_credentials["password"]
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
assert client.token is not None
|
||||
assert len(client.token) > 0
|
||||
@ -26,10 +26,7 @@ async def test_login_success(require_credentials):
|
||||
async def test_login_invalid_credentials():
|
||||
"""E2E: Неверные credentials."""
|
||||
with pytest.raises(KworkAuthError):
|
||||
await KworkClient.login(
|
||||
username="invalid_user_12345",
|
||||
password="invalid_pass_12345"
|
||||
)
|
||||
await KworkClient.login(username="invalid_user_12345", password="invalid_pass_12345")
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
@ -37,12 +34,11 @@ async def test_restore_session(require_credentials):
|
||||
"""E2E: Восстановление сессии из токена."""
|
||||
# First login
|
||||
client1 = await KworkClient.login(
|
||||
username=require_credentials["username"],
|
||||
password=require_credentials["password"]
|
||||
username=require_credentials["username"], password=require_credentials["password"]
|
||||
)
|
||||
token = client1.token
|
||||
await client1.aclose()
|
||||
|
||||
|
||||
# Restore from token
|
||||
client2 = KworkClient(token=token)
|
||||
try:
|
||||
|
||||
@ -6,77 +6,76 @@ Skip these tests in CI/CD or when running unit tests only.
|
||||
|
||||
Usage:
|
||||
pytest tests/integration/ -m integration
|
||||
|
||||
|
||||
Or with credentials:
|
||||
KWORK_USERNAME=user KWORK_PASSWORD=pass pytest tests/integration/ -m integration
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from kwork_api import KworkClient, KworkAuthError
|
||||
from kwork_api import KworkAuthError, KworkClient
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client() -> Optional[KworkClient]:
|
||||
def client() -> KworkClient | None:
|
||||
"""
|
||||
Create authenticated client for integration tests.
|
||||
|
||||
|
||||
Requires KWORK_USERNAME and KWORK_PASSWORD environment variables.
|
||||
Skip tests if not provided.
|
||||
"""
|
||||
username = os.getenv("KWORK_USERNAME")
|
||||
password = os.getenv("KWORK_PASSWORD")
|
||||
|
||||
|
||||
if not username or not password:
|
||||
pytest.skip("KWORK_USERNAME and KWORK_PASSWORD not set")
|
||||
|
||||
|
||||
# Create client
|
||||
import asyncio
|
||||
|
||||
|
||||
async def create_client():
|
||||
return await KworkClient.login(username, password)
|
||||
|
||||
|
||||
return asyncio.run(create_client())
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestAuthentication:
|
||||
"""Test authentication with real API."""
|
||||
|
||||
|
||||
def test_login_with_credentials(self):
|
||||
"""Test login with real credentials."""
|
||||
username = os.getenv("KWORK_USERNAME")
|
||||
password = os.getenv("KWORK_PASSWORD")
|
||||
|
||||
|
||||
if not username or not password:
|
||||
pytest.skip("Credentials not set")
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
async def login():
|
||||
client = await KworkClient.login(username, password)
|
||||
assert client._token is not None
|
||||
assert "userId" in client._cookies
|
||||
await client.close()
|
||||
return True
|
||||
|
||||
|
||||
result = asyncio.run(login())
|
||||
assert result
|
||||
|
||||
|
||||
def test_invalid_credentials(self):
|
||||
"""Test login with invalid credentials."""
|
||||
import asyncio
|
||||
|
||||
|
||||
async def try_login():
|
||||
try:
|
||||
await KworkClient.login("invalid_user_12345", "wrong_password")
|
||||
return False
|
||||
except KworkAuthError:
|
||||
return True
|
||||
|
||||
|
||||
result = asyncio.run(try_login())
|
||||
assert result # Should raise auth error
|
||||
|
||||
@ -84,43 +83,43 @@ class TestAuthentication:
|
||||
@pytest.mark.integration
|
||||
class TestCatalogAPI:
|
||||
"""Test catalog endpoints with real API."""
|
||||
|
||||
|
||||
def test_get_catalog_list(self, client: KworkClient):
|
||||
"""Test getting catalog list."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
async def fetch():
|
||||
result = await client.catalog.get_list(page=1)
|
||||
return result
|
||||
|
||||
|
||||
result = asyncio.run(fetch())
|
||||
|
||||
|
||||
assert result.kworks is not None
|
||||
assert len(result.kworks) > 0
|
||||
assert result.pagination is not None
|
||||
|
||||
|
||||
def test_get_kwork_details(self, client: KworkClient):
|
||||
"""Test getting kwork details."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
async def fetch():
|
||||
# First get a kwork ID from catalog
|
||||
catalog = await client.catalog.get_list(page=1)
|
||||
if not catalog.kworks:
|
||||
return None
|
||||
|
||||
|
||||
kwork_id = catalog.kworks[0].id
|
||||
details = await client.catalog.get_details(kwork_id)
|
||||
return details
|
||||
|
||||
|
||||
result = asyncio.run(fetch())
|
||||
|
||||
|
||||
if result:
|
||||
assert result.id is not None
|
||||
assert result.title is not None
|
||||
@ -130,61 +129,63 @@ class TestCatalogAPI:
|
||||
@pytest.mark.integration
|
||||
class TestProjectsAPI:
|
||||
"""Test projects endpoints with real API."""
|
||||
|
||||
|
||||
def test_get_projects_list(self, client: KworkClient):
|
||||
"""Test getting projects list."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
async def fetch():
|
||||
return await client.projects.get_list(page=1)
|
||||
|
||||
|
||||
result = asyncio.run(fetch())
|
||||
|
||||
|
||||
assert result.projects is not None
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestReferenceAPI:
|
||||
"""Test reference data endpoints."""
|
||||
|
||||
|
||||
def test_get_cities(self, client: KworkClient):
|
||||
"""Test getting cities."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
async def fetch():
|
||||
return await client.reference.get_cities()
|
||||
|
||||
|
||||
result = asyncio.run(fetch())
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
# Kwork has many cities, should have at least some
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
def test_get_countries(self, client: KworkClient):
|
||||
"""Test getting countries."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
result = asyncio.run(client.reference.get_countries())
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
def test_get_timezones(self, client: KworkClient):
|
||||
"""Test getting timezones."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
result = asyncio.run(client.reference.get_timezones())
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) > 0
|
||||
|
||||
@ -192,15 +193,16 @@ class TestReferenceAPI:
|
||||
@pytest.mark.integration
|
||||
class TestUserAPI:
|
||||
"""Test user endpoints."""
|
||||
|
||||
|
||||
def test_get_user_info(self, client: KworkClient):
|
||||
"""Test getting current user info."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
result = asyncio.run(client.user.get_info())
|
||||
|
||||
|
||||
assert isinstance(result, dict)
|
||||
# Should have user data
|
||||
assert result # Not empty
|
||||
@ -209,36 +211,36 @@ class TestUserAPI:
|
||||
@pytest.mark.integration
|
||||
class TestErrorHandling:
|
||||
"""Test error handling with real API."""
|
||||
|
||||
|
||||
def test_invalid_kwork_id(self, client: KworkClient):
|
||||
"""Test getting non-existent kwork."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
async def fetch():
|
||||
try:
|
||||
await client.catalog.get_details(999999999)
|
||||
return False
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
result = asyncio.run(fetch())
|
||||
|
||||
asyncio.run(fetch())
|
||||
# May or may not raise error depending on API behavior
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestRateLimiting:
|
||||
"""Test rate limiting behavior."""
|
||||
|
||||
|
||||
def test_multiple_requests(self, client: KworkClient):
|
||||
"""Test making multiple requests."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
async def fetch_multiple():
|
||||
results = []
|
||||
for page in range(1, 4):
|
||||
@ -247,9 +249,9 @@ class TestRateLimiting:
|
||||
# Small delay to avoid rate limiting
|
||||
await asyncio.sleep(0.5)
|
||||
return results
|
||||
|
||||
|
||||
results = asyncio.run(fetch_multiple())
|
||||
|
||||
|
||||
assert len(results) == 3
|
||||
for result in results:
|
||||
assert result.kworks is not None
|
||||
|
||||
Binary file not shown.
@ -8,25 +8,27 @@ import pytest
|
||||
import respx
|
||||
from httpx import Response
|
||||
|
||||
from kwork_api import KworkClient, KworkAuthError, KworkApiError
|
||||
from kwork_api.models import CatalogResponse, Kwork, ValidationResponse, ValidationIssue
|
||||
from kwork_api import KworkApiError, KworkAuthError, KworkClient, KworkNetworkError
|
||||
from kwork_api.models import CatalogResponse, ValidationResponse
|
||||
|
||||
|
||||
class TestAuthentication:
|
||||
"""Test authentication flows."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_login_success(self):
|
||||
"""Test successful login."""
|
||||
import httpx
|
||||
|
||||
|
||||
# Mock login endpoint
|
||||
login_route = respx.post("https://kwork.ru/signIn")
|
||||
login_route.mock(return_value=httpx.Response(
|
||||
200,
|
||||
headers={"Set-Cookie": "userId=12345; slrememberme=token123"},
|
||||
))
|
||||
|
||||
login_route.mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
headers={"Set-Cookie": "userId=12345; slrememberme=token123"},
|
||||
)
|
||||
)
|
||||
|
||||
# Mock token endpoint
|
||||
token_route = respx.post("https://kwork.ru/getWebAuthToken").mock(
|
||||
return_value=Response(
|
||||
@ -34,53 +36,53 @@ class TestAuthentication:
|
||||
json={"web_auth_token": "test_token_abc123"},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Login
|
||||
client = await KworkClient.login("testuser", "testpass")
|
||||
|
||||
|
||||
# Verify
|
||||
assert login_route.called
|
||||
assert token_route.called
|
||||
assert client._token == "test_token_abc123"
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_login_invalid_credentials(self):
|
||||
"""Test login with invalid credentials."""
|
||||
respx.post("https://kwork.ru/signIn").mock(
|
||||
return_value=Response(401, json={"error": "Invalid credentials"})
|
||||
)
|
||||
|
||||
|
||||
with pytest.raises(KworkAuthError):
|
||||
await KworkClient.login("wrong", "wrong")
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_login_no_userid(self):
|
||||
"""Test login without userId in cookies."""
|
||||
import httpx
|
||||
|
||||
|
||||
respx.post("https://kwork.ru/signIn").mock(
|
||||
return_value=httpx.Response(200, headers={"Set-Cookie": "other=value"})
|
||||
)
|
||||
|
||||
|
||||
with pytest.raises(KworkAuthError, match="no userId"):
|
||||
await KworkClient.login("test", "test")
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_login_no_token(self):
|
||||
"""Test login without web_auth_token in response."""
|
||||
import httpx
|
||||
|
||||
|
||||
respx.post("https://kwork.ru/signIn").mock(
|
||||
return_value=httpx.Response(200, headers={"Set-Cookie": "userId=123"})
|
||||
)
|
||||
|
||||
|
||||
respx.post("https://kwork.ru/getWebAuthToken").mock(
|
||||
return_value=Response(200, json={"other": "data"})
|
||||
)
|
||||
|
||||
|
||||
with pytest.raises(KworkAuthError, match="No web_auth_token"):
|
||||
await KworkClient.login("test", "test")
|
||||
|
||||
|
||||
def test_init_with_token(self):
|
||||
"""Test client initialization with token."""
|
||||
client = KworkClient(token="test_token")
|
||||
@ -89,12 +91,12 @@ class TestAuthentication:
|
||||
|
||||
class TestCatalogAPI:
|
||||
"""Test catalog endpoints."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_catalog(self):
|
||||
"""Test getting catalog list."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"kworks": [
|
||||
{"id": 1, "title": "Test Kwork", "price": 1000.0},
|
||||
@ -106,23 +108,23 @@ class TestCatalogAPI:
|
||||
"total_items": 100,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
respx.post(f"{client.base_url}/catalogMainv2").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
result = await client.catalog.get_list(page=1)
|
||||
|
||||
|
||||
assert isinstance(result, CatalogResponse)
|
||||
assert len(result.kworks) == 2
|
||||
assert result.kworks[0].id == 1
|
||||
assert result.pagination.total_pages == 5
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_kwork_details(self):
|
||||
"""Test getting kwork details."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"id": 123,
|
||||
"title": "Detailed Kwork",
|
||||
@ -130,38 +132,38 @@ class TestCatalogAPI:
|
||||
"full_description": "Full description here",
|
||||
"delivery_time": 3,
|
||||
}
|
||||
|
||||
|
||||
respx.post(f"{client.base_url}/getKworkDetails").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
result = await client.catalog.get_details(123)
|
||||
|
||||
|
||||
assert result.id == 123
|
||||
assert result.full_description == "Full description here"
|
||||
assert result.delivery_time == 3
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_catalog_error(self):
|
||||
"""Test catalog API error handling."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
respx.post(f"{client.base_url}/catalogMainv2").mock(
|
||||
return_value=Response(400, json={"message": "Invalid category"})
|
||||
)
|
||||
|
||||
|
||||
with pytest.raises(KworkApiError):
|
||||
await client.catalog.get_list(category_id=99999)
|
||||
|
||||
|
||||
class TestProjectsAPI:
|
||||
"""Test projects endpoints."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_projects(self):
|
||||
"""Test getting projects list."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"projects": [
|
||||
{
|
||||
@ -174,106 +176,102 @@ class TestProjectsAPI:
|
||||
],
|
||||
"pagination": {"current_page": 1},
|
||||
}
|
||||
|
||||
respx.post(f"{client.base_url}/projects").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{client.base_url}/projects").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.projects.get_list()
|
||||
|
||||
|
||||
assert len(result.projects) == 1
|
||||
assert result.projects[0].budget == 10000.0
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Test error handling."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_404_error(self):
|
||||
"""Test 404 error handling."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
respx.post(f"{client.base_url}/getKworkDetails").mock(
|
||||
return_value=Response(404)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{client.base_url}/getKworkDetails").mock(return_value=Response(404))
|
||||
|
||||
with pytest.raises(KworkApiError) as exc_info:
|
||||
await client.catalog.get_details(999)
|
||||
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_401_error(self):
|
||||
"""Test 401 error handling."""
|
||||
client = KworkClient(token="invalid")
|
||||
|
||||
respx.post(f"{client.base_url}/catalogMainv2").mock(
|
||||
return_value=Response(401)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{client.base_url}/catalogMainv2").mock(return_value=Response(401))
|
||||
|
||||
with pytest.raises(KworkAuthError):
|
||||
await client.catalog.get_list()
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_network_error(self):
|
||||
"""Test network error handling."""
|
||||
import httpx
|
||||
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
respx.post(f"{client.base_url}/catalogMainv2").mock(
|
||||
side_effect=Exception("Connection refused")
|
||||
side_effect=httpx.RequestError("Connection refused", request=None)
|
||||
)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
|
||||
with pytest.raises(KworkNetworkError):
|
||||
await client.catalog.get_list()
|
||||
|
||||
|
||||
class TestContextManager:
|
||||
"""Test async context manager."""
|
||||
|
||||
|
||||
async def test_context_manager(self):
|
||||
"""Test using client as context manager."""
|
||||
async with KworkClient(token="test") as client:
|
||||
assert client._client is None # Not created yet
|
||||
|
||||
|
||||
# Client should be created on first request
|
||||
# (but we don't make actual requests in this test)
|
||||
|
||||
|
||||
# Client should be closed after context
|
||||
assert client._client is None or client._client.is_closed
|
||||
|
||||
|
||||
class TestValidationAPI:
|
||||
"""Test text validation endpoint."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_validate_text_success(self):
|
||||
"""Test successful text validation."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"success": True,
|
||||
"is_valid": True,
|
||||
"issues": [],
|
||||
"score": 95,
|
||||
}
|
||||
|
||||
|
||||
respx.post(f"{client.base_url}/api/validation/checktext").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
result = await client.other.validate_text("Хороший текст для кворка")
|
||||
|
||||
|
||||
assert isinstance(result, ValidationResponse)
|
||||
assert result.success is True
|
||||
assert result.is_valid is True
|
||||
assert len(result.issues) == 0
|
||||
assert result.score == 95
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_validate_text_with_issues(self):
|
||||
"""Test text validation with found issues."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"success": True,
|
||||
"is_valid": False,
|
||||
@ -293,40 +291,40 @@ class TestValidationAPI:
|
||||
],
|
||||
"score": 45,
|
||||
}
|
||||
|
||||
|
||||
respx.post(f"{client.base_url}/api/validation/checktext").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
result = await client.other.validate_text(
|
||||
"Звоните +7-999-000-00-00",
|
||||
context="kwork_description",
|
||||
)
|
||||
|
||||
|
||||
assert result.is_valid is False
|
||||
assert len(result.issues) == 2
|
||||
assert result.issues[0].code == "CONTACT_INFO"
|
||||
assert result.issues[0].type == "error"
|
||||
assert result.issues[1].type == "warning"
|
||||
assert result.score == 45
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_validate_text_empty(self):
|
||||
"""Test validation of empty text."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"success": False,
|
||||
"is_valid": False,
|
||||
"message": "Текст не может быть пустым",
|
||||
"issues": [],
|
||||
}
|
||||
|
||||
|
||||
respx.post(f"{client.base_url}/api/validation/checktext").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
result = await client.other.validate_text("")
|
||||
|
||||
|
||||
assert result.success is False
|
||||
assert result.message is not None
|
||||
|
||||
@ -8,17 +8,17 @@ import pytest
|
||||
import respx
|
||||
from httpx import Response
|
||||
|
||||
from kwork_api import KworkClient, KworkAuthError, KworkApiError
|
||||
from kwork_api import KworkApiError, KworkClient
|
||||
from kwork_api.models import (
|
||||
NotificationsResponse,
|
||||
Kwork,
|
||||
Dialog,
|
||||
Badge,
|
||||
City,
|
||||
Country,
|
||||
TimeZone,
|
||||
Dialog,
|
||||
Feature,
|
||||
Badge,
|
||||
Kwork,
|
||||
NotificationsResponse,
|
||||
Project,
|
||||
TimeZone,
|
||||
)
|
||||
|
||||
BASE_URL = "https://api.kwork.ru"
|
||||
@ -26,22 +26,22 @@ BASE_URL = "https://api.kwork.ru"
|
||||
|
||||
class TestClientProperties:
|
||||
"""Test client properties and initialization."""
|
||||
|
||||
|
||||
def test_token_property(self):
|
||||
"""Test token property getter."""
|
||||
client = KworkClient(token="test_token_123")
|
||||
assert client.token == "test_token_123"
|
||||
|
||||
|
||||
def test_token_property_none(self):
|
||||
"""Test token property when no token."""
|
||||
client = KworkClient()
|
||||
assert client.token is None
|
||||
|
||||
|
||||
def test_cookies_property_empty(self):
|
||||
"""Test cookies property with no cookies."""
|
||||
client = KworkClient(token="test")
|
||||
assert client.cookies == {}
|
||||
|
||||
|
||||
def test_cookies_property_with_cookies(self):
|
||||
"""Test cookies property returns copy."""
|
||||
client = KworkClient(token="test", cookies={"userId": "123", "session": "abc"})
|
||||
@ -49,14 +49,14 @@ class TestClientProperties:
|
||||
assert cookies == {"userId": "123", "session": "abc"}
|
||||
cookies["modified"] = "value"
|
||||
assert "modified" not in client.cookies
|
||||
|
||||
|
||||
def test_credentials_property(self):
|
||||
"""Test credentials property returns token and cookies."""
|
||||
client = KworkClient(token="test_token", cookies={"userId": "123"})
|
||||
creds = client.credentials
|
||||
assert creds["token"] == "test_token"
|
||||
assert creds["cookies"] == {"userId": "123"}
|
||||
|
||||
|
||||
def test_credentials_property_no_cookies(self):
|
||||
"""Test credentials with no cookies."""
|
||||
client = KworkClient(token="test_token")
|
||||
@ -67,136 +67,131 @@ class TestClientProperties:
|
||||
|
||||
class TestCatalogAPIExtended:
|
||||
"""Extended tests for CatalogAPI."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_details_extra(self):
|
||||
"""Test get_details_extra endpoint."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"id": 456,
|
||||
"title": "Extra Details Kwork",
|
||||
"extra_field": "extra_value",
|
||||
}
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/getKworkDetailsExtra").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
result = await client.catalog.get_details_extra(456)
|
||||
|
||||
|
||||
assert result["id"] == 456
|
||||
assert result["extra_field"] == "extra_value"
|
||||
|
||||
|
||||
class TestProjectsAPIExtended:
|
||||
"""Extended tests for ProjectsAPI."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_payer_orders(self):
|
||||
"""Test getting payer orders."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"orders": [
|
||||
{"id": 101, "title": "Order 1", "amount": 5000.0, "status": "active"},
|
||||
]
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/payerOrders").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/payerOrders").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.projects.get_payer_orders()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], Project)
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_worker_orders(self):
|
||||
"""Test getting worker orders."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"orders": [
|
||||
{"id": 202, "title": "Worker Order", "amount": 3000.0, "status": "completed"},
|
||||
]
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/workerOrders").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/workerOrders").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.projects.get_worker_orders()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
class TestUserAPI:
|
||||
"""Tests for UserAPI."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_info(self):
|
||||
"""Test getting user info."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"userId": 12345,
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"balance": 50000.0,
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/user").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/user").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.user.get_info()
|
||||
|
||||
|
||||
assert result["userId"] == 12345
|
||||
assert result["username"] == "testuser"
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_reviews(self):
|
||||
"""Test getting user reviews."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"reviews": [
|
||||
{"id": 1, "rating": 5, "comment": "Great work!", "author": {"id": 999, "username": "client1"}},
|
||||
{
|
||||
"id": 1,
|
||||
"rating": 5,
|
||||
"comment": "Great work!",
|
||||
"author": {"id": 999, "username": "client1"},
|
||||
},
|
||||
],
|
||||
"pagination": {"current_page": 1, "total_pages": 5},
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/userReviews").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/userReviews").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.user.get_reviews(user_id=12345, page=1)
|
||||
|
||||
|
||||
assert len(result.reviews) == 1
|
||||
assert result.reviews[0].rating == 5
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_favorite_kworks(self):
|
||||
"""Test getting favorite kworks."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"kworks": [
|
||||
{"id": 100, "title": "Favorite Kwork 1", "price": 2000.0},
|
||||
{"id": 101, "title": "Favorite Kwork 2", "price": 3000.0},
|
||||
]
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/favoriteKworks").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/favoriteKworks").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.user.get_favorite_kworks()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert isinstance(result[0], Kwork)
|
||||
@ -204,133 +199,129 @@ class TestUserAPI:
|
||||
|
||||
class TestReferenceAPI:
|
||||
"""Tests for ReferenceAPI."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_cities(self):
|
||||
"""Test getting cities list."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"cities": [
|
||||
{"id": 1, "name": "Москва", "country_id": 1},
|
||||
{"id": 2, "name": "Санкт-Петербург", "country_id": 1},
|
||||
]
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/cities").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/cities").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.reference.get_cities()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert isinstance(result[0], City)
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_countries(self):
|
||||
"""Test getting countries list."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"countries": [
|
||||
{"id": 1, "name": "Россия", "code": "RU"},
|
||||
{"id": 2, "name": "Беларусь", "code": "BY"},
|
||||
]
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/countries").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/countries").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.reference.get_countries()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert isinstance(result[0], Country)
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_timezones(self):
|
||||
"""Test getting timezones list."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"timezones": [
|
||||
{"id": 1, "name": "Europe/Moscow", "offset": "+03:00"},
|
||||
{"id": 2, "name": "Europe/Kaliningrad", "offset": "+02:00"},
|
||||
]
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/timezones").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/timezones").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.reference.get_timezones()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert isinstance(result[0], TimeZone)
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_features(self):
|
||||
"""Test getting features list."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"features": [
|
||||
{"id": 1, "name": "Feature 1", "category_id": 5, "price": 1000, "type": "extra"},
|
||||
{"id": 2, "name": "Feature 2", "category_id": 5, "price": 2000, "type": "extra"},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/getAvailableFeatures").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
result = await client.reference.get_features()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert isinstance(result[0], Feature)
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_public_features(self):
|
||||
"""Test getting public features list."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"features": [
|
||||
{"id": 10, "name": "Public Feature", "is_public": True, "price": 500, "type": "extra"},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Public Feature",
|
||||
"is_public": True,
|
||||
"price": 500,
|
||||
"type": "extra",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/getPublicFeatures").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/getPublicFeatures").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.reference.get_public_features()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_badges_info(self):
|
||||
"""Test getting badges info."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"badges": [
|
||||
{"id": 1, "name": "Pro Seller", "icon_url": "https://example.com/badge1.png"},
|
||||
{"id": 2, "name": "Verified", "icon_url": "https://example.com/badge2.png"},
|
||||
]
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/getBadgesInfo").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/getBadgesInfo").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.reference.get_badges_info()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert isinstance(result[0], Badge)
|
||||
@ -338,406 +329,400 @@ class TestReferenceAPI:
|
||||
|
||||
class TestNotificationsAPI:
|
||||
"""Tests for NotificationsAPI."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_list(self):
|
||||
"""Test getting notifications list."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"notifications": [
|
||||
{"id": 1, "type": "order", "title": "New Order", "message": "New order received", "is_read": False},
|
||||
{"id": 2, "type": "message", "title": "New Message", "message": "You have a new message", "is_read": True},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "order",
|
||||
"title": "New Order",
|
||||
"message": "New order received",
|
||||
"is_read": False,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "message",
|
||||
"title": "New Message",
|
||||
"message": "You have a new message",
|
||||
"is_read": True,
|
||||
},
|
||||
],
|
||||
"unread_count": 5,
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/notifications").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/notifications").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.notifications.get_list()
|
||||
|
||||
|
||||
assert isinstance(result, NotificationsResponse)
|
||||
assert result.unread_count == 5
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_fetch(self):
|
||||
"""Test fetching notifications."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"notifications": [
|
||||
{"id": 3, "type": "system", "title": "System Update", "message": "System update available", "is_read": False},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "system",
|
||||
"title": "System Update",
|
||||
"message": "System update available",
|
||||
"is_read": False,
|
||||
},
|
||||
],
|
||||
"unread_count": 1,
|
||||
}
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/notificationsFetch").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
result = await client.notifications.fetch()
|
||||
|
||||
|
||||
assert isinstance(result, NotificationsResponse)
|
||||
assert len(result.notifications) == 1
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_dialogs(self):
|
||||
"""Test getting dialogs."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"dialogs": [
|
||||
{"id": 1, "user_id": 100, "last_message": "Hello", "unread": 2},
|
||||
{"id": 2, "user_id": 200, "last_message": "Hi", "unread": 0},
|
||||
]
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/dialogs").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/dialogs").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.notifications.get_dialogs()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert isinstance(result[0], Dialog)
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_blocked_dialogs(self):
|
||||
"""Test getting blocked dialogs."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"dialogs": [
|
||||
{"id": 99, "user_id": 999, "last_message": "Spam", "blocked": True},
|
||||
]
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/blockedDialogList").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/blockedDialogList").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.notifications.get_blocked_dialogs()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
class TestOtherAPI:
|
||||
"""Tests for OtherAPI."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_wants(self):
|
||||
"""Test getting wants."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"wants": [{"id": 1, "title": "I need a logo"}],
|
||||
"count": 1,
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/myWants").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/myWants").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.other.get_wants()
|
||||
|
||||
|
||||
assert "wants" in result
|
||||
assert result["count"] == 1
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_wants_status(self):
|
||||
"""Test getting wants status."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"active_wants": 5,
|
||||
"completed_wants": 10,
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/wantsStatusList").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/wantsStatusList").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.other.get_wants_status()
|
||||
|
||||
|
||||
assert result["active_wants"] == 5
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_kworks_status(self):
|
||||
"""Test getting kworks status."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"active_kworks": 3,
|
||||
"total_sales": 50,
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/kworksStatusList").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/kworksStatusList").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.other.get_kworks_status()
|
||||
|
||||
|
||||
assert result["active_kworks"] == 3
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_offers(self):
|
||||
"""Test getting offers."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"offers": [{"id": 1, "title": "Special offer"}],
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/offers").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/offers").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.other.get_offers()
|
||||
|
||||
|
||||
assert "offers" in result
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_exchange_info(self):
|
||||
"""Test getting exchange info."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"usd_rate": 90.5,
|
||||
"eur_rate": 98.2,
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/exchangeInfo").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/exchangeInfo").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.other.get_exchange_info()
|
||||
|
||||
|
||||
assert result["usd_rate"] == 90.5
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_channel(self):
|
||||
"""Test getting channel info."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"channel_id": "main",
|
||||
"name": "Main Channel",
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/getChannel").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/getChannel").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.other.get_channel()
|
||||
|
||||
|
||||
assert result["channel_id"] == "main"
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_in_app_notification(self):
|
||||
"""Test getting in-app notifications."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"notifications": [{"id": 1, "message": "App update"}],
|
||||
}
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/getInAppNotification").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
result = await client.other.get_in_app_notification()
|
||||
|
||||
|
||||
assert "notifications" in result
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_security_user_data(self):
|
||||
"""Test getting security user data."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"two_factor_enabled": True,
|
||||
"last_login": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/getSecurityUserData").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
result = await client.other.get_security_user_data()
|
||||
|
||||
|
||||
assert result["two_factor_enabled"] is True
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_is_dialog_allow_true(self):
|
||||
"""Test is_dialog_allow returns True."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/isDialogAllow").mock(
|
||||
return_value=Response(200, json={"allowed": True})
|
||||
)
|
||||
|
||||
|
||||
result = await client.other.is_dialog_allow(12345)
|
||||
|
||||
|
||||
assert result is True
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_is_dialog_allow_false(self):
|
||||
"""Test is_dialog_allow returns False."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/isDialogAllow").mock(
|
||||
return_value=Response(200, json={"allowed": False})
|
||||
)
|
||||
|
||||
|
||||
result = await client.other.is_dialog_allow(67890)
|
||||
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_viewed_kworks(self):
|
||||
"""Test getting viewed kworks."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"kworks": [
|
||||
{"id": 500, "title": "Viewed Kwork", "price": 1500.0},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/viewedCatalogKworks").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
result = await client.other.get_viewed_kworks()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], Kwork)
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_favorite_categories(self):
|
||||
"""Test getting favorite categories."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"categories": [1, 5, 10, 15],
|
||||
}
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/favoriteCategories").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
result = await client.other.get_favorite_categories()
|
||||
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert 1 in result
|
||||
assert 5 in result
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_update_settings(self):
|
||||
"""Test updating settings."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"success": True,
|
||||
"updated": {"notifications_enabled": False},
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/updateSettings").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/updateSettings").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
settings = {"notifications_enabled": False, "theme": "dark"}
|
||||
result = await client.other.update_settings(settings)
|
||||
|
||||
|
||||
assert result["success"] is True
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_go_offline(self):
|
||||
"""Test going offline."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"success": True,
|
||||
"status": "offline",
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/offline").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/offline").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.other.go_offline()
|
||||
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["status"] == "offline"
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_get_actor(self):
|
||||
"""Test getting actor info."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {
|
||||
"actor_id": 123,
|
||||
"name": "Test Actor",
|
||||
}
|
||||
|
||||
respx.post(f"{BASE_URL}/actor").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/actor").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client.other.get_actor()
|
||||
|
||||
|
||||
assert result["actor_id"] == 123
|
||||
|
||||
|
||||
class TestClientInternals:
|
||||
"""Tests for internal client methods."""
|
||||
|
||||
|
||||
def test_handle_response_success(self):
|
||||
"""Test _handle_response with successful response."""
|
||||
import httpx
|
||||
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
response = httpx.Response(200, json={"success": True, "data": "test"})
|
||||
result = client._handle_response(response)
|
||||
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["data"] == "test"
|
||||
|
||||
|
||||
def test_handle_response_error(self):
|
||||
"""Test _handle_response with error response."""
|
||||
import httpx
|
||||
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
response = httpx.Response(400, json={"message": "Bad request"})
|
||||
|
||||
|
||||
with pytest.raises(KworkApiError) as exc_info:
|
||||
client._handle_response(response)
|
||||
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_request_method(self):
|
||||
"""Test _request method directly."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
|
||||
mock_data = {"result": "success"}
|
||||
|
||||
respx.post(f"{BASE_URL}/test-endpoint").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
|
||||
respx.post(f"{BASE_URL}/test-endpoint").mock(return_value=Response(200, json=mock_data))
|
||||
|
||||
result = await client._request("POST", "/test-endpoint", json={"param": "value"})
|
||||
|
||||
|
||||
assert result["result"] == "success"
|
||||
|
||||
|
||||
async def test_context_manager_creates_client(self):
|
||||
"""Test that context manager creates httpx client."""
|
||||
async with KworkClient(token="test") as client:
|
||||
assert client.token == "test"
|
||||
|
||||
|
||||
assert client._client is None or client._client.is_closed
|
||||
|
||||
Loading…
Reference in New Issue
Block a user