fix: auto-format code and fix linter errors
Some checks failed
PR Checks / test (pull_request) Failing after 25s
PR Checks / security (pull_request) Failing after 7s

This commit is contained in:
root 2026-03-29 08:58:35 +00:00
parent b67f0e5031
commit 3995d60b6b
15 changed files with 1914 additions and 809 deletions

View File

@ -5,27 +5,35 @@ Unofficial Python client for Kwork.ru API.
Example: Example:
from kwork_api import KworkClient from kwork_api import KworkClient
# Login with credentials # Login with credentials
client = await KworkClient.login("username", "password") client = await KworkClient.login("username", "password")
# Or restore from token # Or restore from token
client = KworkClient(token="your_web_auth_token") client = KworkClient(token="your_web_auth_token")
# Get catalog # Get catalog
catalog = await client.catalog.get_list(page=1) catalog = await client.catalog.get_list(page=1)
""" """
from .client import KworkClient from .client import KworkClient
from .errors import KworkError, KworkAuthError, KworkApiError from .errors import (
KworkApiError,
KworkAuthError,
KworkError,
KworkNetworkError,
KworkNotFoundError,
KworkRateLimitError,
KworkValidationError,
)
from .models import ( from .models import (
ValidationResponse, CatalogResponse,
ValidationIssue,
Kwork, Kwork,
KworkDetails, KworkDetails,
Project, Project,
CatalogResponse,
ProjectsResponse, ProjectsResponse,
ValidationIssue,
ValidationResponse,
) )
__version__ = "0.1.0" # Updated by semantic-release __version__ = "0.1.0" # Updated by semantic-release
@ -34,6 +42,10 @@ __all__ = [
"KworkError", "KworkError",
"KworkAuthError", "KworkAuthError",
"KworkApiError", "KworkApiError",
"KworkNetworkError",
"KworkNotFoundError",
"KworkRateLimitError",
"KworkValidationError",
"ValidationResponse", "ValidationResponse",
"ValidationIssue", "ValidationIssue",
"Kwork", "Kwork",

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
KworkNetworkError (ошибки сети) KworkNetworkError (ошибки сети)
""" """
from typing import Any, Optional from typing import Any
__all__ = [ __all__ = [
"KworkError", "KworkError",
@ -29,25 +29,25 @@ __all__ = [
class KworkError(Exception): class KworkError(Exception):
""" """
Базовое исключение для всех ошибок Kwork API. Базовое исключение для всех ошибок Kwork API.
Все остальные исключения наследуются от этого класса. Все остальные исключения наследуются от этого класса.
Attributes: Attributes:
message: Сообщение об ошибке. message: Сообщение об ошибке.
response: Оригинальный HTTP response (если есть). response: Оригинальный HTTP response (если есть).
Example: Example:
try: try:
await client.catalog.get_list() await client.catalog.get_list()
except KworkError as e: except KworkError as e:
print(f"Ошибка: {e.message}") 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.message = message
self.response = response self.response = response
super().__init__(self.message) super().__init__(self.message)
def __str__(self) -> str: def __str__(self) -> str:
return f"KworkError: {self.message}" return f"KworkError: {self.message}"
@ -55,22 +55,22 @@ class KworkError(Exception):
class KworkAuthError(KworkError): class KworkAuthError(KworkError):
""" """
Ошибка аутентификации/авторизации. Ошибка аутентификации/авторизации.
Возникает при: Возникает при:
- Неверном логине или пароле - Неверном логине или пароле
- Истёкшем или невалидном токене - Истёкшем или невалидном токене
- Отсутствии прав доступа (403) - Отсутствии прав доступа (403)
Example: Example:
try: try:
client = await KworkClient.login("user", "wrong_password") client = await KworkClient.login("user", "wrong_password")
except KworkAuthError: except KworkAuthError:
print("Неверные учётные данные") 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) super().__init__(message, response)
def __str__(self) -> str: def __str__(self) -> str:
return f"KworkAuthError: {self.message}" return f"KworkAuthError: {self.message}"
@ -78,28 +78,28 @@ class KworkAuthError(KworkError):
class KworkApiError(KworkError): class KworkApiError(KworkError):
""" """
Ошибка HTTP запроса к API (4xx, 5xx). Ошибка HTTP запроса к API (4xx, 5xx).
Базовый класс для HTTP ошибок API. Содержит код статуса. Базовый класс для HTTP ошибок API. Содержит код статуса.
Attributes: Attributes:
status_code: HTTP код ответа (400, 404, 500, etc.) status_code: HTTP код ответа (400, 404, 500, etc.)
Example: Example:
try: try:
await client.catalog.get_details(999999) await client.catalog.get_details(999999)
except KworkApiError as e: except KworkApiError as e:
print(f"HTTP {e.status_code}: {e.message}") print(f"HTTP {e.status_code}: {e.message}")
""" """
def __init__( def __init__(
self, self,
message: str, message: str,
status_code: Optional[int] = None, status_code: int | None = None,
response: Optional[Any] = None, response: Any | None = None,
): ):
self.status_code = status_code self.status_code = status_code
super().__init__(message, response) super().__init__(message, response)
def __str__(self) -> str: def __str__(self) -> str:
if self.status_code: if self.status_code:
return f"KworkApiError [{self.status_code}]: {self.message}" return f"KworkApiError [{self.status_code}]: {self.message}"
@ -109,46 +109,46 @@ class KworkApiError(KworkError):
class KworkNotFoundError(KworkApiError): class KworkNotFoundError(KworkApiError):
""" """
Ресурс не найден (404). Ресурс не найден (404).
Возникает при запросе несуществующего кворка, Возникает при запросе несуществующего кворка,
пользователя или другого ресурса. пользователя или другого ресурса.
Example: Example:
try: try:
await client.catalog.get_details(999999) await client.catalog.get_details(999999)
except KworkNotFoundError: except KworkNotFoundError:
print("Кворк не найден") 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) super().__init__(f"Resource not found: {resource}", 404, response)
class KworkRateLimitError(KworkApiError): class KworkRateLimitError(KworkApiError):
""" """
Превышен лимит запросов (429). Превышен лимит запросов (429).
Возникает при слишком частых запросах к API. Возникает при слишком частых запросах к API.
Рекомендуется сделать паузу перед повторным запросом. Рекомендуется сделать паузу перед повторным запросом.
Attributes: Attributes:
retry_after: Время ожидания в секундах (если указано сервером). retry_after: Время ожидания в секундах (если указано сервером).
Example: Example:
import asyncio import asyncio
try: try:
await client.catalog.get_list() await client.catalog.get_list()
except KworkRateLimitError as e: except KworkRateLimitError as e:
wait_time = e.retry_after or 5 wait_time = e.retry_after or 5
await asyncio.sleep(wait_time) await asyncio.sleep(wait_time)
""" """
def __init__( def __init__(
self, self,
message: str = "Rate limit exceeded", message: str = "Rate limit exceeded",
response: Optional[Any] = None, response: Any | None = None,
retry_after: Optional[int] = None, retry_after: int | None = None,
): ):
self.retry_after = retry_after self.retry_after = retry_after
super().__init__(message, 429, response) super().__init__(message, 429, response)
@ -157,12 +157,12 @@ class KworkRateLimitError(KworkApiError):
class KworkValidationError(KworkApiError): class KworkValidationError(KworkApiError):
""" """
Ошибка валидации (400). Ошибка валидации (400).
Возникает при некорректных данных запроса. Возникает при некорректных данных запроса.
Attributes: Attributes:
fields: Словарь ошибок по полям {field: [errors]}. fields: Словарь ошибок по полям {field: [errors]}.
Example: Example:
try: try:
await client.catalog.get_list(page=-1) await client.catalog.get_list(page=-1)
@ -171,16 +171,16 @@ class KworkValidationError(KworkApiError):
for field, errors in e.fields.items(): for field, errors in e.fields.items():
print(f"{field}: {errors[0]}") print(f"{field}: {errors[0]}")
""" """
def __init__( def __init__(
self, self,
message: str = "Validation failed", message: str = "Validation failed",
fields: Optional[dict[str, list[str]]] = None, fields: dict[str, list[str]] | None = None,
response: Optional[Any] = None, response: Any | None = None,
): ):
self.fields = fields or {} self.fields = fields or {}
super().__init__(message, 400, response) super().__init__(message, 400, response)
def __str__(self) -> str: def __str__(self) -> str:
if self.fields: if self.fields:
field_errors = ", ".join(f"{k}: {v[0]}" for k, v in self.fields.items()) field_errors = ", ".join(f"{k}: {v[0]}" for k, v in self.fields.items())
@ -191,22 +191,22 @@ class KworkValidationError(KworkApiError):
class KworkNetworkError(KworkError): class KworkNetworkError(KworkError):
""" """
Ошибка сети/подключения. Ошибка сети/подключения.
Возникает при: Возникает при:
- Отсутствии соединения - Отсутствии соединения
- Таймауте запроса - Таймауте запроса
- Ошибке DNS - Ошибке DNS
- Проблемах с SSL - Проблемах с SSL
Example: Example:
try: try:
await client.catalog.get_list() await client.catalog.get_list()
except KworkNetworkError: except KworkNetworkError:
print("Проверьте подключение к интернету") 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) super().__init__(message, response)
def __str__(self) -> str: def __str__(self) -> str:
return f"KworkNetworkError: {self.message}" return f"KworkNetworkError: {self.message}"

View File

@ -6,7 +6,7 @@ Pydantic модели для ответов Kwork API.
""" """
from datetime import datetime from datetime import datetime
from typing import Any, Optional from typing import Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -14,47 +14,49 @@ from pydantic import BaseModel, Field
class KworkUser(BaseModel): class KworkUser(BaseModel):
""" """
Информация о пользователе Kwork. Информация о пользователе Kwork.
Attributes: Attributes:
id: Уникальный ID пользователя. id: Уникальный ID пользователя.
username: Имя пользователя (логин). username: Имя пользователя (логин).
avatar_url: URL аватара или None. avatar_url: URL аватара или None.
is_online: Статус онлайн. is_online: Статус онлайн.
rating: Рейтинг пользователя (0-5). rating: Рейтинг пользователя (0-5).
Example: Example:
user = KworkUser(id=123, username="seller", rating=4.9) user = KworkUser(id=123, username="seller", rating=4.9)
print(f"{user.username}: {user.rating}") print(f"{user.username}: {user.rating}")
""" """
id: int id: int
username: str username: str
avatar_url: Optional[str] = None avatar_url: str | None = None
is_online: bool = False is_online: bool = False
rating: Optional[float] = None rating: float | None = None
class KworkCategory(BaseModel): class KworkCategory(BaseModel):
""" """
Категория кворков. Категория кворков.
Attributes: Attributes:
id: Уникальный ID категории. id: Уникальный ID категории.
name: Название категории. name: Название категории.
slug: URL-safe идентификатор. slug: URL-safe идентификатор.
parent_id: ID родительской категории для вложенности. parent_id: ID родительской категории для вложенности.
""" """
id: int id: int
name: str name: str
slug: str slug: str
parent_id: Optional[int] = None parent_id: int | None = None
class Kwork(BaseModel): class Kwork(BaseModel):
""" """
Кворк услуга на Kwork. Кворк услуга на Kwork.
Базовая модель кворка с основной информацией. Базовая модель кворка с основной информацией.
Attributes: Attributes:
id: Уникальный ID кворка. id: Уникальный ID кворка.
title: Заголовок кворка. title: Заголовок кворка.
@ -69,26 +71,27 @@ class Kwork(BaseModel):
created_at: Дата создания. created_at: Дата создания.
updated_at: Дата последнего обновления. updated_at: Дата последнего обновления.
""" """
id: int id: int
title: str title: str
description: Optional[str] = None description: str | None = None
price: float price: float
currency: str = "RUB" currency: str = "RUB"
category_id: Optional[int] = None category_id: int | None = None
seller: Optional[KworkUser] = None seller: KworkUser | None = None
images: list[str] = Field(default_factory=list) images: list[str] = Field(default_factory=list)
rating: Optional[float] = None rating: float | None = None
reviews_count: int = 0 reviews_count: int = 0
created_at: Optional[datetime] = None created_at: datetime | None = None
updated_at: Optional[datetime] = None updated_at: datetime | None = None
class KworkDetails(Kwork): class KworkDetails(Kwork):
""" """
Расширенная информация о кворке. Расширенная информация о кворке.
Наследует все поля Kwork плюс дополнительные детали. Наследует все поля Kwork плюс дополнительные детали.
Attributes: Attributes:
full_description: Полное описание услуги. full_description: Полное описание услуги.
requirements: Требования к заказчику. requirements: Требования к заказчику.
@ -97,10 +100,11 @@ class KworkDetails(Kwork):
features: Список дополнительных опций. features: Список дополнительных опций.
faq: Список вопросов и ответов. faq: Список вопросов и ответов.
""" """
full_description: Optional[str] = None
requirements: Optional[str] = None full_description: str | None = None
delivery_time: Optional[int] = None requirements: str | None = None
revisions: Optional[int] = None delivery_time: int | None = None
revisions: int | None = None
features: list[str] = Field(default_factory=list) features: list[str] = Field(default_factory=list)
faq: list[dict[str, str]] = Field(default_factory=list) faq: list[dict[str, str]] = Field(default_factory=list)
@ -108,7 +112,7 @@ class KworkDetails(Kwork):
class PaginationInfo(BaseModel): class PaginationInfo(BaseModel):
""" """
Информация о пагинации. Информация о пагинации.
Attributes: Attributes:
current_page: Текущая страница (начиная с 1). current_page: Текущая страница (начиная с 1).
total_pages: Общее количество страниц. total_pages: Общее количество страниц.
@ -117,6 +121,7 @@ class PaginationInfo(BaseModel):
has_next: Есть ли следующая страница. has_next: Есть ли следующая страница.
has_prev: Есть ли предыдущая страница. has_prev: Есть ли предыдущая страница.
""" """
current_page: int = 1 current_page: int = 1
total_pages: int = 1 total_pages: int = 1
total_items: int = 0 total_items: int = 0
@ -128,23 +133,24 @@ class PaginationInfo(BaseModel):
class CatalogResponse(BaseModel): class CatalogResponse(BaseModel):
""" """
Ответ API каталога кворков. Ответ API каталога кворков.
Attributes: Attributes:
kworks: Список кворков на странице. kworks: Список кворков на странице.
pagination: Информация о пагинации. pagination: Информация о пагинации.
filters: Доступные фильтры. filters: Доступные фильтры.
sort_options: Доступные опции сортировки. sort_options: Доступные опции сортировки.
""" """
kworks: list[Kwork] = Field(default_factory=list) kworks: list[Kwork] = Field(default_factory=list)
pagination: Optional[PaginationInfo] = None pagination: PaginationInfo | None = None
filters: Optional[dict[str, Any]] = None filters: dict[str, Any] | None = None
sort_options: list[str] = Field(default_factory=list) sort_options: list[str] = Field(default_factory=list)
class Project(BaseModel): class Project(BaseModel):
""" """
Проект (заказ на бирже фриланса). Проект (заказ на бирже фриланса).
Attributes: Attributes:
id: Уникальный ID проекта. id: Уникальный ID проекта.
title: Заголовок проекта. title: Заголовок проекта.
@ -159,16 +165,17 @@ class Project(BaseModel):
bids_count: Количество откликов. bids_count: Количество откликов.
skills: Требуемые навыки. skills: Требуемые навыки.
""" """
id: int id: int
title: str title: str
description: Optional[str] = None description: str | None = None
budget: Optional[float] = None budget: float | None = None
budget_type: str = "fixed" budget_type: str = "fixed"
category_id: Optional[int] = None category_id: int | None = None
customer: Optional[KworkUser] = None customer: KworkUser | None = None
status: str = "open" status: str = "open"
created_at: Optional[datetime] = None created_at: datetime | None = None
updated_at: Optional[datetime] = None updated_at: datetime | None = None
bids_count: int = 0 bids_count: int = 0
skills: list[str] = Field(default_factory=list) skills: list[str] = Field(default_factory=list)
@ -176,19 +183,20 @@ class Project(BaseModel):
class ProjectsResponse(BaseModel): class ProjectsResponse(BaseModel):
""" """
Ответ API списка проектов. Ответ API списка проектов.
Attributes: Attributes:
projects: Список проектов. projects: Список проектов.
pagination: Информация о пагинации. pagination: Информация о пагинации.
""" """
projects: list[Project] = Field(default_factory=list) projects: list[Project] = Field(default_factory=list)
pagination: Optional[PaginationInfo] = None pagination: PaginationInfo | None = None
class Review(BaseModel): class Review(BaseModel):
""" """
Отзыв о кворке или проекте. Отзыв о кворке или проекте.
Attributes: Attributes:
id: Уникальный ID отзыва. id: Уникальный ID отзыва.
rating: Оценка от 1 до 5. rating: Оценка от 1 до 5.
@ -197,32 +205,34 @@ class Review(BaseModel):
kwork_id: ID кворка (если отзыв о кворке). kwork_id: ID кворка (если отзыв о кворке).
created_at: Дата создания. created_at: Дата создания.
""" """
id: int id: int
rating: int = Field(ge=1, le=5) rating: int = Field(ge=1, le=5)
comment: Optional[str] = None comment: str | None = None
author: Optional[KworkUser] = None author: KworkUser | None = None
kwork_id: Optional[int] = None kwork_id: int | None = None
created_at: Optional[datetime] = None created_at: datetime | None = None
class ReviewsResponse(BaseModel): class ReviewsResponse(BaseModel):
""" """
Ответ API списка отзывов. Ответ API списка отзывов.
Attributes: Attributes:
reviews: Список отзывов. reviews: Список отзывов.
pagination: Информация о пагинации. pagination: Информация о пагинации.
average_rating: Средний рейтинг. average_rating: Средний рейтинг.
""" """
reviews: list[Review] = Field(default_factory=list) reviews: list[Review] = Field(default_factory=list)
pagination: Optional[PaginationInfo] = None pagination: PaginationInfo | None = None
average_rating: Optional[float] = None average_rating: float | None = None
class Notification(BaseModel): class Notification(BaseModel):
""" """
Уведомление пользователя. Уведомление пользователя.
Attributes: Attributes:
id: Уникальный ID уведомления. id: Уникальный ID уведомления.
type: Тип уведомления: "message", "order", "system", etc. type: Тип уведомления: "message", "order", "system", etc.
@ -232,23 +242,25 @@ class Notification(BaseModel):
created_at: Дата создания. created_at: Дата создания.
link: Ссылка для перехода (если есть). link: Ссылка для перехода (если есть).
""" """
id: int id: int
type: str type: str
title: str title: str
message: str message: str
is_read: bool = False is_read: bool = False
created_at: Optional[datetime] = None created_at: datetime | None = None
link: Optional[str] = None link: str | None = None
class NotificationsResponse(BaseModel): class NotificationsResponse(BaseModel):
""" """
Ответ API списка уведомлений. Ответ API списка уведомлений.
Attributes: Attributes:
notifications: Список уведомлений. notifications: Список уведомлений.
unread_count: Количество непрочитанных уведомлений. unread_count: Количество непрочитанных уведомлений.
""" """
notifications: list[Notification] = Field(default_factory=list) notifications: list[Notification] = Field(default_factory=list)
unread_count: int = 0 unread_count: int = 0
@ -256,7 +268,7 @@ class NotificationsResponse(BaseModel):
class Dialog(BaseModel): class Dialog(BaseModel):
""" """
Диалог (чат) с пользователем. Диалог (чат) с пользователем.
Attributes: Attributes:
id: Уникальный ID диалога. id: Уникальный ID диалога.
participant: Собеседник. participant: Собеседник.
@ -264,17 +276,18 @@ class Dialog(BaseModel):
unread_count: Количество непрочитанных сообщений. unread_count: Количество непрочитанных сообщений.
updated_at: Время последнего сообщения. updated_at: Время последнего сообщения.
""" """
id: int id: int
participant: Optional[KworkUser] = None participant: KworkUser | None = None
last_message: Optional[str] = None last_message: str | None = None
unread_count: int = 0 unread_count: int = 0
updated_at: Optional[datetime] = None updated_at: datetime | None = None
class AuthResponse(BaseModel): class AuthResponse(BaseModel):
""" """
Ответ API аутентификации. Ответ API аутентификации.
Attributes: Attributes:
success: Успешность аутентификации. success: Успешность аутентификации.
user_id: ID пользователя. user_id: ID пользователя.
@ -282,80 +295,86 @@ class AuthResponse(BaseModel):
web_auth_token: Токен для последующих запросов. web_auth_token: Токен для последующих запросов.
message: Сообщение (например, об ошибке). message: Сообщение (например, об ошибке).
""" """
success: bool success: bool
user_id: Optional[int] = None user_id: int | None = None
username: Optional[str] = None username: str | None = None
web_auth_token: Optional[str] = None web_auth_token: str | None = None
message: Optional[str] = None message: str | None = None
class ErrorDetail(BaseModel): class ErrorDetail(BaseModel):
""" """
Детали ошибки API. Детали ошибки API.
Attributes: Attributes:
code: Код ошибки. code: Код ошибки.
message: Сообщение об ошибке. message: Сообщение об ошибке.
field: Поле, вызвавшее ошибку (если применимо). field: Поле, вызвавшее ошибку (если применимо).
""" """
code: str code: str
message: str message: str
field: Optional[str] = None field: str | None = None
class APIErrorResponse(BaseModel): class APIErrorResponse(BaseModel):
""" """
Стандартный ответ API об ошибке. Стандартный ответ API об ошибке.
Attributes: Attributes:
success: Всегда False для ошибок. success: Всегда False для ошибок.
errors: Список деталей ошибок. errors: Список деталей ошибок.
message: Общее сообщение об ошибке. message: Общее сообщение об ошибке.
""" """
success: bool = False success: bool = False
errors: list[ErrorDetail] = Field(default_factory=list) errors: list[ErrorDetail] = Field(default_factory=list)
message: Optional[str] = None message: str | None = None
class City(BaseModel): class City(BaseModel):
""" """
Город из справочника. Город из справочника.
Attributes: Attributes:
id: Уникальный ID города. id: Уникальный ID города.
name: Название города. name: Название города.
country_id: ID страны. country_id: ID страны.
""" """
id: int id: int
name: str name: str
country_id: Optional[int] = None country_id: int | None = None
class Country(BaseModel): class Country(BaseModel):
""" """
Страна из справочника. Страна из справочника.
Attributes: Attributes:
id: Уникальный ID страны. id: Уникальный ID страны.
name: Название страны. name: Название страны.
code: Код страны (ISO). code: Код страны (ISO).
cities: Список городов в стране. cities: Список городов в стране.
""" """
id: int id: int
name: str name: str
code: Optional[str] = None code: str | None = None
cities: list[City] = Field(default_factory=list) cities: list[City] = Field(default_factory=list)
class TimeZone(BaseModel): class TimeZone(BaseModel):
""" """
Часовой пояс. Часовой пояс.
Attributes: Attributes:
id: Уникальный ID. id: Уникальный ID.
name: Название пояса. name: Название пояса.
offset: Смещение от UTC (например, "+03:00"). offset: Смещение от UTC (например, "+03:00").
""" """
id: int id: int
name: str name: str
offset: str offset: str
@ -364,7 +383,7 @@ class TimeZone(BaseModel):
class Feature(BaseModel): class Feature(BaseModel):
""" """
Дополнительная функция (feature) для кворка. Дополнительная функция (feature) для кворка.
Attributes: Attributes:
id: Уникальный ID функции. id: Уникальный ID функции.
name: Название. name: Название.
@ -372,9 +391,10 @@ class Feature(BaseModel):
price: Стоимость в рублях. price: Стоимость в рублях.
type: Тип: "extra", "premium", etc. type: Тип: "extra", "premium", etc.
""" """
id: int id: int
name: str name: str
description: Optional[str] = None description: str | None = None
price: float price: float
type: str type: str
@ -382,40 +402,42 @@ class Feature(BaseModel):
class Badge(BaseModel): class Badge(BaseModel):
""" """
Значок (достижение) пользователя. Значок (достижение) пользователя.
Attributes: Attributes:
id: Уникальный ID значка. id: Уникальный ID значка.
name: Название значка. name: Название значка.
description: Описание достижения. description: Описание достижения.
icon_url: URL иконки значка. icon_url: URL иконки значка.
""" """
id: int id: int
name: str name: str
description: Optional[str] = None description: str | None = None
icon_url: Optional[str] = None icon_url: str | None = None
# Generic response wrapper # Generic response wrapper
class DataResponse(BaseModel): class DataResponse(BaseModel):
""" """
Универсальный ответ API с данными. Универсальный ответ API с данными.
Используется как обёртка для различных ответов API. Используется как обёртка для различных ответов API.
Attributes: Attributes:
success: Успешность запроса. success: Успешность запроса.
data: Полезные данные (словарь). data: Полезные данные (словарь).
message: Дополнительное сообщение. message: Дополнительное сообщение.
""" """
success: bool = True success: bool = True
data: Optional[dict[str, Any]] = None data: dict[str, Any] | None = None
message: Optional[str] = None message: str | None = None
class ValidationIssue(BaseModel): class ValidationIssue(BaseModel):
""" """
Проблема, найденная при валидации текста. Проблема, найденная при валидации текста.
Attributes: Attributes:
type: Тип проблемы: "error", "warning", "suggestion". type: Тип проблемы: "error", "warning", "suggestion".
code: Код ошибки (например, "SPELLING", "GRAMMAR", "LENGTH"). code: Код ошибки (например, "SPELLING", "GRAMMAR", "LENGTH").
@ -423,19 +445,20 @@ class ValidationIssue(BaseModel):
position: Позиция в тексте (если применимо). position: Позиция в тексте (если применимо).
suggestion: Предлагаемое исправление (если есть). suggestion: Предлагаемое исправление (если есть).
""" """
type: str = "error" type: str = "error"
code: str code: str
message: str message: str
position: Optional[int] = None position: int | None = None
suggestion: Optional[str] = None suggestion: str | None = None
class ValidationResponse(BaseModel): class ValidationResponse(BaseModel):
""" """
Ответ API валидации текста. Ответ API валидации текста.
Используется для эндпоинта /api/validation/checktext. Используется для эндпоинта /api/validation/checktext.
Attributes: Attributes:
success: Успешность валидации. success: Успешность валидации.
is_valid: Текст проходит валидацию (нет критических ошибок). is_valid: Текст проходит валидацию (нет критических ошибок).
@ -443,8 +466,9 @@ class ValidationResponse(BaseModel):
score: Оценка качества текста (0-100, если доступна). score: Оценка качества текста (0-100, если доступна).
message: Дополнительное сообщение. message: Дополнительное сообщение.
""" """
success: bool = True success: bool = True
is_valid: bool = True is_valid: bool = True
issues: list[ValidationIssue] = Field(default_factory=list) issues: list[ValidationIssue] = Field(default_factory=list)
score: Optional[int] = None score: int | None = None
message: Optional[str] = None message: str | None = None

1094
test-results/report.html Normal file

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,9 @@ E2E тесты для Kwork API.
""" """
import os import os
import pytest
from pathlib import Path from pathlib import Path
import pytest
from dotenv import load_dotenv from dotenv import load_dotenv
# Загружаем .env # Загружаем .env
@ -39,21 +40,17 @@ def slowmo(request):
slowmo = request.config.getoption("--slowmo", default=0) slowmo = request.config.getoption("--slowmo", default=0)
if slowmo > 0: if slowmo > 0:
import time import time
time.sleep(slowmo) time.sleep(slowmo)
def pytest_configure(config): def pytest_configure(config):
"""Регистрация маркера e2e.""" """Регистрация маркера e2e."""
config.addinivalue_line( config.addinivalue_line("markers", "e2e: mark test as end-to-end (requires credentials)")
"markers", "e2e: mark test as end-to-end (requires credentials)"
)
def pytest_addoption(parser): def pytest_addoption(parser):
"""Добавляет опцию --slowmo.""" """Добавляет опцию --slowmo."""
parser.addoption( parser.addoption(
"--slowmo", "--slowmo", type=float, default=0, help="Delay between tests in seconds (for rate limiting)"
type=float,
default=0,
help="Delay between tests in seconds (for rate limiting)"
) )

View File

@ -3,6 +3,7 @@ E2E тесты аутентификации.
""" """
import pytest import pytest
from kwork_api import KworkClient from kwork_api import KworkClient
from kwork_api.errors import KworkAuthError from kwork_api.errors import KworkAuthError
@ -11,10 +12,9 @@ from kwork_api.errors import KworkAuthError
async def test_login_success(require_credentials): async def test_login_success(require_credentials):
"""E2E: Успешная аутентификация.""" """E2E: Успешная аутентификация."""
client = await KworkClient.login( client = await KworkClient.login(
username=require_credentials["username"], username=require_credentials["username"], password=require_credentials["password"]
password=require_credentials["password"]
) )
try: try:
assert client.token is not None assert client.token is not None
assert len(client.token) > 0 assert len(client.token) > 0
@ -26,10 +26,7 @@ async def test_login_success(require_credentials):
async def test_login_invalid_credentials(): async def test_login_invalid_credentials():
"""E2E: Неверные credentials.""" """E2E: Неверные credentials."""
with pytest.raises(KworkAuthError): with pytest.raises(KworkAuthError):
await KworkClient.login( await KworkClient.login(username="invalid_user_12345", password="invalid_pass_12345")
username="invalid_user_12345",
password="invalid_pass_12345"
)
@pytest.mark.e2e @pytest.mark.e2e
@ -37,12 +34,11 @@ async def test_restore_session(require_credentials):
"""E2E: Восстановление сессии из токена.""" """E2E: Восстановление сессии из токена."""
# First login # First login
client1 = await KworkClient.login( client1 = await KworkClient.login(
username=require_credentials["username"], username=require_credentials["username"], password=require_credentials["password"]
password=require_credentials["password"]
) )
token = client1.token token = client1.token
await client1.aclose() await client1.aclose()
# Restore from token # Restore from token
client2 = KworkClient(token=token) client2 = KworkClient(token=token)
try: try:

View File

@ -6,77 +6,76 @@ Skip these tests in CI/CD or when running unit tests only.
Usage: Usage:
pytest tests/integration/ -m integration pytest tests/integration/ -m integration
Or with credentials: Or with credentials:
KWORK_USERNAME=user KWORK_PASSWORD=pass pytest tests/integration/ -m integration KWORK_USERNAME=user KWORK_PASSWORD=pass pytest tests/integration/ -m integration
""" """
import os import os
from typing import Optional
import pytest import pytest
from kwork_api import KworkClient, KworkAuthError from kwork_api import KworkAuthError, KworkClient
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def client() -> Optional[KworkClient]: def client() -> KworkClient | None:
""" """
Create authenticated client for integration tests. Create authenticated client for integration tests.
Requires KWORK_USERNAME and KWORK_PASSWORD environment variables. Requires KWORK_USERNAME and KWORK_PASSWORD environment variables.
Skip tests if not provided. Skip tests if not provided.
""" """
username = os.getenv("KWORK_USERNAME") username = os.getenv("KWORK_USERNAME")
password = os.getenv("KWORK_PASSWORD") password = os.getenv("KWORK_PASSWORD")
if not username or not password: if not username or not password:
pytest.skip("KWORK_USERNAME and KWORK_PASSWORD not set") pytest.skip("KWORK_USERNAME and KWORK_PASSWORD not set")
# Create client # Create client
import asyncio import asyncio
async def create_client(): async def create_client():
return await KworkClient.login(username, password) return await KworkClient.login(username, password)
return asyncio.run(create_client()) return asyncio.run(create_client())
@pytest.mark.integration @pytest.mark.integration
class TestAuthentication: class TestAuthentication:
"""Test authentication with real API.""" """Test authentication with real API."""
def test_login_with_credentials(self): def test_login_with_credentials(self):
"""Test login with real credentials.""" """Test login with real credentials."""
username = os.getenv("KWORK_USERNAME") username = os.getenv("KWORK_USERNAME")
password = os.getenv("KWORK_PASSWORD") password = os.getenv("KWORK_PASSWORD")
if not username or not password: if not username or not password:
pytest.skip("Credentials not set") pytest.skip("Credentials not set")
import asyncio import asyncio
async def login(): async def login():
client = await KworkClient.login(username, password) client = await KworkClient.login(username, password)
assert client._token is not None assert client._token is not None
assert "userId" in client._cookies assert "userId" in client._cookies
await client.close() await client.close()
return True return True
result = asyncio.run(login()) result = asyncio.run(login())
assert result assert result
def test_invalid_credentials(self): def test_invalid_credentials(self):
"""Test login with invalid credentials.""" """Test login with invalid credentials."""
import asyncio import asyncio
async def try_login(): async def try_login():
try: try:
await KworkClient.login("invalid_user_12345", "wrong_password") await KworkClient.login("invalid_user_12345", "wrong_password")
return False return False
except KworkAuthError: except KworkAuthError:
return True return True
result = asyncio.run(try_login()) result = asyncio.run(try_login())
assert result # Should raise auth error assert result # Should raise auth error
@ -84,43 +83,43 @@ class TestAuthentication:
@pytest.mark.integration @pytest.mark.integration
class TestCatalogAPI: class TestCatalogAPI:
"""Test catalog endpoints with real API.""" """Test catalog endpoints with real API."""
def test_get_catalog_list(self, client: KworkClient): def test_get_catalog_list(self, client: KworkClient):
"""Test getting catalog list.""" """Test getting catalog list."""
if not client: if not client:
pytest.skip("No client") pytest.skip("No client")
import asyncio import asyncio
async def fetch(): async def fetch():
result = await client.catalog.get_list(page=1) result = await client.catalog.get_list(page=1)
return result return result
result = asyncio.run(fetch()) result = asyncio.run(fetch())
assert result.kworks is not None assert result.kworks is not None
assert len(result.kworks) > 0 assert len(result.kworks) > 0
assert result.pagination is not None assert result.pagination is not None
def test_get_kwork_details(self, client: KworkClient): def test_get_kwork_details(self, client: KworkClient):
"""Test getting kwork details.""" """Test getting kwork details."""
if not client: if not client:
pytest.skip("No client") pytest.skip("No client")
import asyncio import asyncio
async def fetch(): async def fetch():
# First get a kwork ID from catalog # First get a kwork ID from catalog
catalog = await client.catalog.get_list(page=1) catalog = await client.catalog.get_list(page=1)
if not catalog.kworks: if not catalog.kworks:
return None return None
kwork_id = catalog.kworks[0].id kwork_id = catalog.kworks[0].id
details = await client.catalog.get_details(kwork_id) details = await client.catalog.get_details(kwork_id)
return details return details
result = asyncio.run(fetch()) result = asyncio.run(fetch())
if result: if result:
assert result.id is not None assert result.id is not None
assert result.title is not None assert result.title is not None
@ -130,61 +129,63 @@ class TestCatalogAPI:
@pytest.mark.integration @pytest.mark.integration
class TestProjectsAPI: class TestProjectsAPI:
"""Test projects endpoints with real API.""" """Test projects endpoints with real API."""
def test_get_projects_list(self, client: KworkClient): def test_get_projects_list(self, client: KworkClient):
"""Test getting projects list.""" """Test getting projects list."""
if not client: if not client:
pytest.skip("No client") pytest.skip("No client")
import asyncio import asyncio
async def fetch(): async def fetch():
return await client.projects.get_list(page=1) return await client.projects.get_list(page=1)
result = asyncio.run(fetch()) result = asyncio.run(fetch())
assert result.projects is not None assert result.projects is not None
@pytest.mark.integration @pytest.mark.integration
class TestReferenceAPI: class TestReferenceAPI:
"""Test reference data endpoints.""" """Test reference data endpoints."""
def test_get_cities(self, client: KworkClient): def test_get_cities(self, client: KworkClient):
"""Test getting cities.""" """Test getting cities."""
if not client: if not client:
pytest.skip("No client") pytest.skip("No client")
import asyncio import asyncio
async def fetch(): async def fetch():
return await client.reference.get_cities() return await client.reference.get_cities()
result = asyncio.run(fetch()) result = asyncio.run(fetch())
assert isinstance(result, list) assert isinstance(result, list)
# Kwork has many cities, should have at least some # Kwork has many cities, should have at least some
assert len(result) > 0 assert len(result) > 0
def test_get_countries(self, client: KworkClient): def test_get_countries(self, client: KworkClient):
"""Test getting countries.""" """Test getting countries."""
if not client: if not client:
pytest.skip("No client") pytest.skip("No client")
import asyncio import asyncio
result = asyncio.run(client.reference.get_countries()) result = asyncio.run(client.reference.get_countries())
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) > 0 assert len(result) > 0
def test_get_timezones(self, client: KworkClient): def test_get_timezones(self, client: KworkClient):
"""Test getting timezones.""" """Test getting timezones."""
if not client: if not client:
pytest.skip("No client") pytest.skip("No client")
import asyncio import asyncio
result = asyncio.run(client.reference.get_timezones()) result = asyncio.run(client.reference.get_timezones())
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) > 0 assert len(result) > 0
@ -192,15 +193,16 @@ class TestReferenceAPI:
@pytest.mark.integration @pytest.mark.integration
class TestUserAPI: class TestUserAPI:
"""Test user endpoints.""" """Test user endpoints."""
def test_get_user_info(self, client: KworkClient): def test_get_user_info(self, client: KworkClient):
"""Test getting current user info.""" """Test getting current user info."""
if not client: if not client:
pytest.skip("No client") pytest.skip("No client")
import asyncio import asyncio
result = asyncio.run(client.user.get_info()) result = asyncio.run(client.user.get_info())
assert isinstance(result, dict) assert isinstance(result, dict)
# Should have user data # Should have user data
assert result # Not empty assert result # Not empty
@ -209,36 +211,36 @@ class TestUserAPI:
@pytest.mark.integration @pytest.mark.integration
class TestErrorHandling: class TestErrorHandling:
"""Test error handling with real API.""" """Test error handling with real API."""
def test_invalid_kwork_id(self, client: KworkClient): def test_invalid_kwork_id(self, client: KworkClient):
"""Test getting non-existent kwork.""" """Test getting non-existent kwork."""
if not client: if not client:
pytest.skip("No client") pytest.skip("No client")
import asyncio import asyncio
async def fetch(): async def fetch():
try: try:
await client.catalog.get_details(999999999) await client.catalog.get_details(999999999)
return False return False
except Exception: except Exception:
return True return True
result = asyncio.run(fetch()) asyncio.run(fetch())
# May or may not raise error depending on API behavior # May or may not raise error depending on API behavior
@pytest.mark.integration @pytest.mark.integration
class TestRateLimiting: class TestRateLimiting:
"""Test rate limiting behavior.""" """Test rate limiting behavior."""
def test_multiple_requests(self, client: KworkClient): def test_multiple_requests(self, client: KworkClient):
"""Test making multiple requests.""" """Test making multiple requests."""
if not client: if not client:
pytest.skip("No client") pytest.skip("No client")
import asyncio import asyncio
async def fetch_multiple(): async def fetch_multiple():
results = [] results = []
for page in range(1, 4): for page in range(1, 4):
@ -247,9 +249,9 @@ class TestRateLimiting:
# Small delay to avoid rate limiting # Small delay to avoid rate limiting
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
return results return results
results = asyncio.run(fetch_multiple()) results = asyncio.run(fetch_multiple())
assert len(results) == 3 assert len(results) == 3
for result in results: for result in results:
assert result.kworks is not None assert result.kworks is not None

View File

@ -8,25 +8,27 @@ import pytest
import respx import respx
from httpx import Response from httpx import Response
from kwork_api import KworkClient, KworkAuthError, KworkApiError from kwork_api import KworkApiError, KworkAuthError, KworkClient, KworkNetworkError
from kwork_api.models import CatalogResponse, Kwork, ValidationResponse, ValidationIssue from kwork_api.models import CatalogResponse, ValidationResponse
class TestAuthentication: class TestAuthentication:
"""Test authentication flows.""" """Test authentication flows."""
@respx.mock @respx.mock
async def test_login_success(self): async def test_login_success(self):
"""Test successful login.""" """Test successful login."""
import httpx import httpx
# Mock login endpoint # Mock login endpoint
login_route = respx.post("https://kwork.ru/signIn") login_route = respx.post("https://kwork.ru/signIn")
login_route.mock(return_value=httpx.Response( login_route.mock(
200, return_value=httpx.Response(
headers={"Set-Cookie": "userId=12345; slrememberme=token123"}, 200,
)) headers={"Set-Cookie": "userId=12345; slrememberme=token123"},
)
)
# Mock token endpoint # Mock token endpoint
token_route = respx.post("https://kwork.ru/getWebAuthToken").mock( token_route = respx.post("https://kwork.ru/getWebAuthToken").mock(
return_value=Response( return_value=Response(
@ -34,53 +36,53 @@ class TestAuthentication:
json={"web_auth_token": "test_token_abc123"}, json={"web_auth_token": "test_token_abc123"},
) )
) )
# Login # Login
client = await KworkClient.login("testuser", "testpass") client = await KworkClient.login("testuser", "testpass")
# Verify # Verify
assert login_route.called assert login_route.called
assert token_route.called assert token_route.called
assert client._token == "test_token_abc123" assert client._token == "test_token_abc123"
@respx.mock @respx.mock
async def test_login_invalid_credentials(self): async def test_login_invalid_credentials(self):
"""Test login with invalid credentials.""" """Test login with invalid credentials."""
respx.post("https://kwork.ru/signIn").mock( respx.post("https://kwork.ru/signIn").mock(
return_value=Response(401, json={"error": "Invalid credentials"}) return_value=Response(401, json={"error": "Invalid credentials"})
) )
with pytest.raises(KworkAuthError): with pytest.raises(KworkAuthError):
await KworkClient.login("wrong", "wrong") await KworkClient.login("wrong", "wrong")
@respx.mock @respx.mock
async def test_login_no_userid(self): async def test_login_no_userid(self):
"""Test login without userId in cookies.""" """Test login without userId in cookies."""
import httpx import httpx
respx.post("https://kwork.ru/signIn").mock( respx.post("https://kwork.ru/signIn").mock(
return_value=httpx.Response(200, headers={"Set-Cookie": "other=value"}) return_value=httpx.Response(200, headers={"Set-Cookie": "other=value"})
) )
with pytest.raises(KworkAuthError, match="no userId"): with pytest.raises(KworkAuthError, match="no userId"):
await KworkClient.login("test", "test") await KworkClient.login("test", "test")
@respx.mock @respx.mock
async def test_login_no_token(self): async def test_login_no_token(self):
"""Test login without web_auth_token in response.""" """Test login without web_auth_token in response."""
import httpx import httpx
respx.post("https://kwork.ru/signIn").mock( respx.post("https://kwork.ru/signIn").mock(
return_value=httpx.Response(200, headers={"Set-Cookie": "userId=123"}) return_value=httpx.Response(200, headers={"Set-Cookie": "userId=123"})
) )
respx.post("https://kwork.ru/getWebAuthToken").mock( respx.post("https://kwork.ru/getWebAuthToken").mock(
return_value=Response(200, json={"other": "data"}) return_value=Response(200, json={"other": "data"})
) )
with pytest.raises(KworkAuthError, match="No web_auth_token"): with pytest.raises(KworkAuthError, match="No web_auth_token"):
await KworkClient.login("test", "test") await KworkClient.login("test", "test")
def test_init_with_token(self): def test_init_with_token(self):
"""Test client initialization with token.""" """Test client initialization with token."""
client = KworkClient(token="test_token") client = KworkClient(token="test_token")
@ -89,12 +91,12 @@ class TestAuthentication:
class TestCatalogAPI: class TestCatalogAPI:
"""Test catalog endpoints.""" """Test catalog endpoints."""
@respx.mock @respx.mock
async def test_get_catalog(self): async def test_get_catalog(self):
"""Test getting catalog list.""" """Test getting catalog list."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"kworks": [ "kworks": [
{"id": 1, "title": "Test Kwork", "price": 1000.0}, {"id": 1, "title": "Test Kwork", "price": 1000.0},
@ -106,23 +108,23 @@ class TestCatalogAPI:
"total_items": 100, "total_items": 100,
}, },
} }
respx.post(f"{client.base_url}/catalogMainv2").mock( respx.post(f"{client.base_url}/catalogMainv2").mock(
return_value=Response(200, json=mock_data) return_value=Response(200, json=mock_data)
) )
result = await client.catalog.get_list(page=1) result = await client.catalog.get_list(page=1)
assert isinstance(result, CatalogResponse) assert isinstance(result, CatalogResponse)
assert len(result.kworks) == 2 assert len(result.kworks) == 2
assert result.kworks[0].id == 1 assert result.kworks[0].id == 1
assert result.pagination.total_pages == 5 assert result.pagination.total_pages == 5
@respx.mock @respx.mock
async def test_get_kwork_details(self): async def test_get_kwork_details(self):
"""Test getting kwork details.""" """Test getting kwork details."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"id": 123, "id": 123,
"title": "Detailed Kwork", "title": "Detailed Kwork",
@ -130,38 +132,38 @@ class TestCatalogAPI:
"full_description": "Full description here", "full_description": "Full description here",
"delivery_time": 3, "delivery_time": 3,
} }
respx.post(f"{client.base_url}/getKworkDetails").mock( respx.post(f"{client.base_url}/getKworkDetails").mock(
return_value=Response(200, json=mock_data) return_value=Response(200, json=mock_data)
) )
result = await client.catalog.get_details(123) result = await client.catalog.get_details(123)
assert result.id == 123 assert result.id == 123
assert result.full_description == "Full description here" assert result.full_description == "Full description here"
assert result.delivery_time == 3 assert result.delivery_time == 3
@respx.mock @respx.mock
async def test_catalog_error(self): async def test_catalog_error(self):
"""Test catalog API error handling.""" """Test catalog API error handling."""
client = KworkClient(token="test") client = KworkClient(token="test")
respx.post(f"{client.base_url}/catalogMainv2").mock( respx.post(f"{client.base_url}/catalogMainv2").mock(
return_value=Response(400, json={"message": "Invalid category"}) return_value=Response(400, json={"message": "Invalid category"})
) )
with pytest.raises(KworkApiError): with pytest.raises(KworkApiError):
await client.catalog.get_list(category_id=99999) await client.catalog.get_list(category_id=99999)
class TestProjectsAPI: class TestProjectsAPI:
"""Test projects endpoints.""" """Test projects endpoints."""
@respx.mock @respx.mock
async def test_get_projects(self): async def test_get_projects(self):
"""Test getting projects list.""" """Test getting projects list."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"projects": [ "projects": [
{ {
@ -174,106 +176,102 @@ class TestProjectsAPI:
], ],
"pagination": {"current_page": 1}, "pagination": {"current_page": 1},
} }
respx.post(f"{client.base_url}/projects").mock( respx.post(f"{client.base_url}/projects").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.projects.get_list() result = await client.projects.get_list()
assert len(result.projects) == 1 assert len(result.projects) == 1
assert result.projects[0].budget == 10000.0 assert result.projects[0].budget == 10000.0
class TestErrorHandling: class TestErrorHandling:
"""Test error handling.""" """Test error handling."""
@respx.mock @respx.mock
async def test_404_error(self): async def test_404_error(self):
"""Test 404 error handling.""" """Test 404 error handling."""
client = KworkClient(token="test") client = KworkClient(token="test")
respx.post(f"{client.base_url}/getKworkDetails").mock( respx.post(f"{client.base_url}/getKworkDetails").mock(return_value=Response(404))
return_value=Response(404)
)
with pytest.raises(KworkApiError) as exc_info: with pytest.raises(KworkApiError) as exc_info:
await client.catalog.get_details(999) await client.catalog.get_details(999)
assert exc_info.value.status_code == 404 assert exc_info.value.status_code == 404
@respx.mock @respx.mock
async def test_401_error(self): async def test_401_error(self):
"""Test 401 error handling.""" """Test 401 error handling."""
client = KworkClient(token="invalid") client = KworkClient(token="invalid")
respx.post(f"{client.base_url}/catalogMainv2").mock( respx.post(f"{client.base_url}/catalogMainv2").mock(return_value=Response(401))
return_value=Response(401)
)
with pytest.raises(KworkAuthError): with pytest.raises(KworkAuthError):
await client.catalog.get_list() await client.catalog.get_list()
@respx.mock @respx.mock
async def test_network_error(self): async def test_network_error(self):
"""Test network error handling.""" """Test network error handling."""
import httpx
client = KworkClient(token="test") client = KworkClient(token="test")
respx.post(f"{client.base_url}/catalogMainv2").mock( 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() await client.catalog.get_list()
class TestContextManager: class TestContextManager:
"""Test async context manager.""" """Test async context manager."""
async def test_context_manager(self): async def test_context_manager(self):
"""Test using client as context manager.""" """Test using client as context manager."""
async with KworkClient(token="test") as client: async with KworkClient(token="test") as client:
assert client._client is None # Not created yet assert client._client is None # Not created yet
# Client should be created on first request # Client should be created on first request
# (but we don't make actual requests in this test) # (but we don't make actual requests in this test)
# Client should be closed after context # Client should be closed after context
assert client._client is None or client._client.is_closed assert client._client is None or client._client.is_closed
class TestValidationAPI: class TestValidationAPI:
"""Test text validation endpoint.""" """Test text validation endpoint."""
@respx.mock @respx.mock
async def test_validate_text_success(self): async def test_validate_text_success(self):
"""Test successful text validation.""" """Test successful text validation."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"success": True, "success": True,
"is_valid": True, "is_valid": True,
"issues": [], "issues": [],
"score": 95, "score": 95,
} }
respx.post(f"{client.base_url}/api/validation/checktext").mock( respx.post(f"{client.base_url}/api/validation/checktext").mock(
return_value=Response(200, json=mock_data) return_value=Response(200, json=mock_data)
) )
result = await client.other.validate_text("Хороший текст для кворка") result = await client.other.validate_text("Хороший текст для кворка")
assert isinstance(result, ValidationResponse) assert isinstance(result, ValidationResponse)
assert result.success is True assert result.success is True
assert result.is_valid is True assert result.is_valid is True
assert len(result.issues) == 0 assert len(result.issues) == 0
assert result.score == 95 assert result.score == 95
@respx.mock @respx.mock
async def test_validate_text_with_issues(self): async def test_validate_text_with_issues(self):
"""Test text validation with found issues.""" """Test text validation with found issues."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"success": True, "success": True,
"is_valid": False, "is_valid": False,
@ -293,40 +291,40 @@ class TestValidationAPI:
], ],
"score": 45, "score": 45,
} }
respx.post(f"{client.base_url}/api/validation/checktext").mock( respx.post(f"{client.base_url}/api/validation/checktext").mock(
return_value=Response(200, json=mock_data) return_value=Response(200, json=mock_data)
) )
result = await client.other.validate_text( result = await client.other.validate_text(
"Звоните +7-999-000-00-00", "Звоните +7-999-000-00-00",
context="kwork_description", context="kwork_description",
) )
assert result.is_valid is False assert result.is_valid is False
assert len(result.issues) == 2 assert len(result.issues) == 2
assert result.issues[0].code == "CONTACT_INFO" assert result.issues[0].code == "CONTACT_INFO"
assert result.issues[0].type == "error" assert result.issues[0].type == "error"
assert result.issues[1].type == "warning" assert result.issues[1].type == "warning"
assert result.score == 45 assert result.score == 45
@respx.mock @respx.mock
async def test_validate_text_empty(self): async def test_validate_text_empty(self):
"""Test validation of empty text.""" """Test validation of empty text."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"success": False, "success": False,
"is_valid": False, "is_valid": False,
"message": "Текст не может быть пустым", "message": "Текст не может быть пустым",
"issues": [], "issues": [],
} }
respx.post(f"{client.base_url}/api/validation/checktext").mock( respx.post(f"{client.base_url}/api/validation/checktext").mock(
return_value=Response(200, json=mock_data) return_value=Response(200, json=mock_data)
) )
result = await client.other.validate_text("") result = await client.other.validate_text("")
assert result.success is False assert result.success is False
assert result.message is not None assert result.message is not None

View File

@ -8,17 +8,17 @@ import pytest
import respx import respx
from httpx import Response from httpx import Response
from kwork_api import KworkClient, KworkAuthError, KworkApiError from kwork_api import KworkApiError, KworkClient
from kwork_api.models import ( from kwork_api.models import (
NotificationsResponse, Badge,
Kwork,
Dialog,
City, City,
Country, Country,
TimeZone, Dialog,
Feature, Feature,
Badge, Kwork,
NotificationsResponse,
Project, Project,
TimeZone,
) )
BASE_URL = "https://api.kwork.ru" BASE_URL = "https://api.kwork.ru"
@ -26,22 +26,22 @@ BASE_URL = "https://api.kwork.ru"
class TestClientProperties: class TestClientProperties:
"""Test client properties and initialization.""" """Test client properties and initialization."""
def test_token_property(self): def test_token_property(self):
"""Test token property getter.""" """Test token property getter."""
client = KworkClient(token="test_token_123") client = KworkClient(token="test_token_123")
assert client.token == "test_token_123" assert client.token == "test_token_123"
def test_token_property_none(self): def test_token_property_none(self):
"""Test token property when no token.""" """Test token property when no token."""
client = KworkClient() client = KworkClient()
assert client.token is None assert client.token is None
def test_cookies_property_empty(self): def test_cookies_property_empty(self):
"""Test cookies property with no cookies.""" """Test cookies property with no cookies."""
client = KworkClient(token="test") client = KworkClient(token="test")
assert client.cookies == {} assert client.cookies == {}
def test_cookies_property_with_cookies(self): def test_cookies_property_with_cookies(self):
"""Test cookies property returns copy.""" """Test cookies property returns copy."""
client = KworkClient(token="test", cookies={"userId": "123", "session": "abc"}) client = KworkClient(token="test", cookies={"userId": "123", "session": "abc"})
@ -49,14 +49,14 @@ class TestClientProperties:
assert cookies == {"userId": "123", "session": "abc"} assert cookies == {"userId": "123", "session": "abc"}
cookies["modified"] = "value" cookies["modified"] = "value"
assert "modified" not in client.cookies assert "modified" not in client.cookies
def test_credentials_property(self): def test_credentials_property(self):
"""Test credentials property returns token and cookies.""" """Test credentials property returns token and cookies."""
client = KworkClient(token="test_token", cookies={"userId": "123"}) client = KworkClient(token="test_token", cookies={"userId": "123"})
creds = client.credentials creds = client.credentials
assert creds["token"] == "test_token" assert creds["token"] == "test_token"
assert creds["cookies"] == {"userId": "123"} assert creds["cookies"] == {"userId": "123"}
def test_credentials_property_no_cookies(self): def test_credentials_property_no_cookies(self):
"""Test credentials with no cookies.""" """Test credentials with no cookies."""
client = KworkClient(token="test_token") client = KworkClient(token="test_token")
@ -67,136 +67,131 @@ class TestClientProperties:
class TestCatalogAPIExtended: class TestCatalogAPIExtended:
"""Extended tests for CatalogAPI.""" """Extended tests for CatalogAPI."""
@respx.mock @respx.mock
async def test_get_details_extra(self): async def test_get_details_extra(self):
"""Test get_details_extra endpoint.""" """Test get_details_extra endpoint."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"id": 456, "id": 456,
"title": "Extra Details Kwork", "title": "Extra Details Kwork",
"extra_field": "extra_value", "extra_field": "extra_value",
} }
respx.post(f"{BASE_URL}/getKworkDetailsExtra").mock( respx.post(f"{BASE_URL}/getKworkDetailsExtra").mock(
return_value=Response(200, json=mock_data) return_value=Response(200, json=mock_data)
) )
result = await client.catalog.get_details_extra(456) result = await client.catalog.get_details_extra(456)
assert result["id"] == 456 assert result["id"] == 456
assert result["extra_field"] == "extra_value" assert result["extra_field"] == "extra_value"
class TestProjectsAPIExtended: class TestProjectsAPIExtended:
"""Extended tests for ProjectsAPI.""" """Extended tests for ProjectsAPI."""
@respx.mock @respx.mock
async def test_get_payer_orders(self): async def test_get_payer_orders(self):
"""Test getting payer orders.""" """Test getting payer orders."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"orders": [ "orders": [
{"id": 101, "title": "Order 1", "amount": 5000.0, "status": "active"}, {"id": 101, "title": "Order 1", "amount": 5000.0, "status": "active"},
] ]
} }
respx.post(f"{BASE_URL}/payerOrders").mock( respx.post(f"{BASE_URL}/payerOrders").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.projects.get_payer_orders() result = await client.projects.get_payer_orders()
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 1 assert len(result) == 1
assert isinstance(result[0], Project) assert isinstance(result[0], Project)
@respx.mock @respx.mock
async def test_get_worker_orders(self): async def test_get_worker_orders(self):
"""Test getting worker orders.""" """Test getting worker orders."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"orders": [ "orders": [
{"id": 202, "title": "Worker Order", "amount": 3000.0, "status": "completed"}, {"id": 202, "title": "Worker Order", "amount": 3000.0, "status": "completed"},
] ]
} }
respx.post(f"{BASE_URL}/workerOrders").mock( respx.post(f"{BASE_URL}/workerOrders").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.projects.get_worker_orders() result = await client.projects.get_worker_orders()
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 1 assert len(result) == 1
class TestUserAPI: class TestUserAPI:
"""Tests for UserAPI.""" """Tests for UserAPI."""
@respx.mock @respx.mock
async def test_get_info(self): async def test_get_info(self):
"""Test getting user info.""" """Test getting user info."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"userId": 12345, "userId": 12345,
"username": "testuser", "username": "testuser",
"email": "test@example.com", "email": "test@example.com",
"balance": 50000.0, "balance": 50000.0,
} }
respx.post(f"{BASE_URL}/user").mock( respx.post(f"{BASE_URL}/user").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.user.get_info() result = await client.user.get_info()
assert result["userId"] == 12345 assert result["userId"] == 12345
assert result["username"] == "testuser" assert result["username"] == "testuser"
@respx.mock @respx.mock
async def test_get_reviews(self): async def test_get_reviews(self):
"""Test getting user reviews.""" """Test getting user reviews."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"reviews": [ "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}, "pagination": {"current_page": 1, "total_pages": 5},
} }
respx.post(f"{BASE_URL}/userReviews").mock( respx.post(f"{BASE_URL}/userReviews").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.user.get_reviews(user_id=12345, page=1) result = await client.user.get_reviews(user_id=12345, page=1)
assert len(result.reviews) == 1 assert len(result.reviews) == 1
assert result.reviews[0].rating == 5 assert result.reviews[0].rating == 5
@respx.mock @respx.mock
async def test_get_favorite_kworks(self): async def test_get_favorite_kworks(self):
"""Test getting favorite kworks.""" """Test getting favorite kworks."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"kworks": [ "kworks": [
{"id": 100, "title": "Favorite Kwork 1", "price": 2000.0}, {"id": 100, "title": "Favorite Kwork 1", "price": 2000.0},
{"id": 101, "title": "Favorite Kwork 2", "price": 3000.0}, {"id": 101, "title": "Favorite Kwork 2", "price": 3000.0},
] ]
} }
respx.post(f"{BASE_URL}/favoriteKworks").mock( respx.post(f"{BASE_URL}/favoriteKworks").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.user.get_favorite_kworks() result = await client.user.get_favorite_kworks()
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 2 assert len(result) == 2
assert isinstance(result[0], Kwork) assert isinstance(result[0], Kwork)
@ -204,133 +199,129 @@ class TestUserAPI:
class TestReferenceAPI: class TestReferenceAPI:
"""Tests for ReferenceAPI.""" """Tests for ReferenceAPI."""
@respx.mock @respx.mock
async def test_get_cities(self): async def test_get_cities(self):
"""Test getting cities list.""" """Test getting cities list."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"cities": [ "cities": [
{"id": 1, "name": "Москва", "country_id": 1}, {"id": 1, "name": "Москва", "country_id": 1},
{"id": 2, "name": "Санкт-Петербург", "country_id": 1}, {"id": 2, "name": "Санкт-Петербург", "country_id": 1},
] ]
} }
respx.post(f"{BASE_URL}/cities").mock( respx.post(f"{BASE_URL}/cities").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_cities() result = await client.reference.get_cities()
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 2 assert len(result) == 2
assert isinstance(result[0], City) assert isinstance(result[0], City)
@respx.mock @respx.mock
async def test_get_countries(self): async def test_get_countries(self):
"""Test getting countries list.""" """Test getting countries list."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"countries": [ "countries": [
{"id": 1, "name": "Россия", "code": "RU"}, {"id": 1, "name": "Россия", "code": "RU"},
{"id": 2, "name": "Беларусь", "code": "BY"}, {"id": 2, "name": "Беларусь", "code": "BY"},
] ]
} }
respx.post(f"{BASE_URL}/countries").mock( respx.post(f"{BASE_URL}/countries").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_countries() result = await client.reference.get_countries()
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 2 assert len(result) == 2
assert isinstance(result[0], Country) assert isinstance(result[0], Country)
@respx.mock @respx.mock
async def test_get_timezones(self): async def test_get_timezones(self):
"""Test getting timezones list.""" """Test getting timezones list."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"timezones": [ "timezones": [
{"id": 1, "name": "Europe/Moscow", "offset": "+03:00"}, {"id": 1, "name": "Europe/Moscow", "offset": "+03:00"},
{"id": 2, "name": "Europe/Kaliningrad", "offset": "+02:00"}, {"id": 2, "name": "Europe/Kaliningrad", "offset": "+02:00"},
] ]
} }
respx.post(f"{BASE_URL}/timezones").mock( respx.post(f"{BASE_URL}/timezones").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_timezones() result = await client.reference.get_timezones()
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 2 assert len(result) == 2
assert isinstance(result[0], TimeZone) assert isinstance(result[0], TimeZone)
@respx.mock @respx.mock
async def test_get_features(self): async def test_get_features(self):
"""Test getting features list.""" """Test getting features list."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"features": [ "features": [
{"id": 1, "name": "Feature 1", "category_id": 5, "price": 1000, "type": "extra"}, {"id": 1, "name": "Feature 1", "category_id": 5, "price": 1000, "type": "extra"},
{"id": 2, "name": "Feature 2", "category_id": 5, "price": 2000, "type": "extra"}, {"id": 2, "name": "Feature 2", "category_id": 5, "price": 2000, "type": "extra"},
] ]
} }
respx.post(f"{BASE_URL}/getAvailableFeatures").mock( respx.post(f"{BASE_URL}/getAvailableFeatures").mock(
return_value=Response(200, json=mock_data) return_value=Response(200, json=mock_data)
) )
result = await client.reference.get_features() result = await client.reference.get_features()
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 2 assert len(result) == 2
assert isinstance(result[0], Feature) assert isinstance(result[0], Feature)
@respx.mock @respx.mock
async def test_get_public_features(self): async def test_get_public_features(self):
"""Test getting public features list.""" """Test getting public features list."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"features": [ "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( respx.post(f"{BASE_URL}/getPublicFeatures").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_public_features() result = await client.reference.get_public_features()
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 1 assert len(result) == 1
@respx.mock @respx.mock
async def test_get_badges_info(self): async def test_get_badges_info(self):
"""Test getting badges info.""" """Test getting badges info."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"badges": [ "badges": [
{"id": 1, "name": "Pro Seller", "icon_url": "https://example.com/badge1.png"}, {"id": 1, "name": "Pro Seller", "icon_url": "https://example.com/badge1.png"},
{"id": 2, "name": "Verified", "icon_url": "https://example.com/badge2.png"}, {"id": 2, "name": "Verified", "icon_url": "https://example.com/badge2.png"},
] ]
} }
respx.post(f"{BASE_URL}/getBadgesInfo").mock( respx.post(f"{BASE_URL}/getBadgesInfo").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_badges_info() result = await client.reference.get_badges_info()
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 2 assert len(result) == 2
assert isinstance(result[0], Badge) assert isinstance(result[0], Badge)
@ -338,406 +329,400 @@ class TestReferenceAPI:
class TestNotificationsAPI: class TestNotificationsAPI:
"""Tests for NotificationsAPI.""" """Tests for NotificationsAPI."""
@respx.mock @respx.mock
async def test_get_list(self): async def test_get_list(self):
"""Test getting notifications list.""" """Test getting notifications list."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"notifications": [ "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, "unread_count": 5,
} }
respx.post(f"{BASE_URL}/notifications").mock( respx.post(f"{BASE_URL}/notifications").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.notifications.get_list() result = await client.notifications.get_list()
assert isinstance(result, NotificationsResponse) assert isinstance(result, NotificationsResponse)
assert result.unread_count == 5 assert result.unread_count == 5
@respx.mock @respx.mock
async def test_fetch(self): async def test_fetch(self):
"""Test fetching notifications.""" """Test fetching notifications."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"notifications": [ "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, "unread_count": 1,
} }
respx.post(f"{BASE_URL}/notificationsFetch").mock( respx.post(f"{BASE_URL}/notificationsFetch").mock(
return_value=Response(200, json=mock_data) return_value=Response(200, json=mock_data)
) )
result = await client.notifications.fetch() result = await client.notifications.fetch()
assert isinstance(result, NotificationsResponse) assert isinstance(result, NotificationsResponse)
assert len(result.notifications) == 1 assert len(result.notifications) == 1
@respx.mock @respx.mock
async def test_get_dialogs(self): async def test_get_dialogs(self):
"""Test getting dialogs.""" """Test getting dialogs."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"dialogs": [ "dialogs": [
{"id": 1, "user_id": 100, "last_message": "Hello", "unread": 2}, {"id": 1, "user_id": 100, "last_message": "Hello", "unread": 2},
{"id": 2, "user_id": 200, "last_message": "Hi", "unread": 0}, {"id": 2, "user_id": 200, "last_message": "Hi", "unread": 0},
] ]
} }
respx.post(f"{BASE_URL}/dialogs").mock( respx.post(f"{BASE_URL}/dialogs").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.notifications.get_dialogs() result = await client.notifications.get_dialogs()
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 2 assert len(result) == 2
assert isinstance(result[0], Dialog) assert isinstance(result[0], Dialog)
@respx.mock @respx.mock
async def test_get_blocked_dialogs(self): async def test_get_blocked_dialogs(self):
"""Test getting blocked dialogs.""" """Test getting blocked dialogs."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"dialogs": [ "dialogs": [
{"id": 99, "user_id": 999, "last_message": "Spam", "blocked": True}, {"id": 99, "user_id": 999, "last_message": "Spam", "blocked": True},
] ]
} }
respx.post(f"{BASE_URL}/blockedDialogList").mock( respx.post(f"{BASE_URL}/blockedDialogList").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.notifications.get_blocked_dialogs() result = await client.notifications.get_blocked_dialogs()
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 1 assert len(result) == 1
class TestOtherAPI: class TestOtherAPI:
"""Tests for OtherAPI.""" """Tests for OtherAPI."""
@respx.mock @respx.mock
async def test_get_wants(self): async def test_get_wants(self):
"""Test getting wants.""" """Test getting wants."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"wants": [{"id": 1, "title": "I need a logo"}], "wants": [{"id": 1, "title": "I need a logo"}],
"count": 1, "count": 1,
} }
respx.post(f"{BASE_URL}/myWants").mock( respx.post(f"{BASE_URL}/myWants").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_wants() result = await client.other.get_wants()
assert "wants" in result assert "wants" in result
assert result["count"] == 1 assert result["count"] == 1
@respx.mock @respx.mock
async def test_get_wants_status(self): async def test_get_wants_status(self):
"""Test getting wants status.""" """Test getting wants status."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"active_wants": 5, "active_wants": 5,
"completed_wants": 10, "completed_wants": 10,
} }
respx.post(f"{BASE_URL}/wantsStatusList").mock( respx.post(f"{BASE_URL}/wantsStatusList").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_wants_status() result = await client.other.get_wants_status()
assert result["active_wants"] == 5 assert result["active_wants"] == 5
@respx.mock @respx.mock
async def test_get_kworks_status(self): async def test_get_kworks_status(self):
"""Test getting kworks status.""" """Test getting kworks status."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"active_kworks": 3, "active_kworks": 3,
"total_sales": 50, "total_sales": 50,
} }
respx.post(f"{BASE_URL}/kworksStatusList").mock( respx.post(f"{BASE_URL}/kworksStatusList").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_kworks_status() result = await client.other.get_kworks_status()
assert result["active_kworks"] == 3 assert result["active_kworks"] == 3
@respx.mock @respx.mock
async def test_get_offers(self): async def test_get_offers(self):
"""Test getting offers.""" """Test getting offers."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"offers": [{"id": 1, "title": "Special offer"}], "offers": [{"id": 1, "title": "Special offer"}],
} }
respx.post(f"{BASE_URL}/offers").mock( respx.post(f"{BASE_URL}/offers").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_offers() result = await client.other.get_offers()
assert "offers" in result assert "offers" in result
@respx.mock @respx.mock
async def test_get_exchange_info(self): async def test_get_exchange_info(self):
"""Test getting exchange info.""" """Test getting exchange info."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"usd_rate": 90.5, "usd_rate": 90.5,
"eur_rate": 98.2, "eur_rate": 98.2,
} }
respx.post(f"{BASE_URL}/exchangeInfo").mock( respx.post(f"{BASE_URL}/exchangeInfo").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_exchange_info() result = await client.other.get_exchange_info()
assert result["usd_rate"] == 90.5 assert result["usd_rate"] == 90.5
@respx.mock @respx.mock
async def test_get_channel(self): async def test_get_channel(self):
"""Test getting channel info.""" """Test getting channel info."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"channel_id": "main", "channel_id": "main",
"name": "Main Channel", "name": "Main Channel",
} }
respx.post(f"{BASE_URL}/getChannel").mock( respx.post(f"{BASE_URL}/getChannel").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_channel() result = await client.other.get_channel()
assert result["channel_id"] == "main" assert result["channel_id"] == "main"
@respx.mock @respx.mock
async def test_get_in_app_notification(self): async def test_get_in_app_notification(self):
"""Test getting in-app notifications.""" """Test getting in-app notifications."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"notifications": [{"id": 1, "message": "App update"}], "notifications": [{"id": 1, "message": "App update"}],
} }
respx.post(f"{BASE_URL}/getInAppNotification").mock( respx.post(f"{BASE_URL}/getInAppNotification").mock(
return_value=Response(200, json=mock_data) return_value=Response(200, json=mock_data)
) )
result = await client.other.get_in_app_notification() result = await client.other.get_in_app_notification()
assert "notifications" in result assert "notifications" in result
@respx.mock @respx.mock
async def test_get_security_user_data(self): async def test_get_security_user_data(self):
"""Test getting security user data.""" """Test getting security user data."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"two_factor_enabled": True, "two_factor_enabled": True,
"last_login": "2024-01-01T00:00:00Z", "last_login": "2024-01-01T00:00:00Z",
} }
respx.post(f"{BASE_URL}/getSecurityUserData").mock( respx.post(f"{BASE_URL}/getSecurityUserData").mock(
return_value=Response(200, json=mock_data) return_value=Response(200, json=mock_data)
) )
result = await client.other.get_security_user_data() result = await client.other.get_security_user_data()
assert result["two_factor_enabled"] is True assert result["two_factor_enabled"] is True
@respx.mock @respx.mock
async def test_is_dialog_allow_true(self): async def test_is_dialog_allow_true(self):
"""Test is_dialog_allow returns True.""" """Test is_dialog_allow returns True."""
client = KworkClient(token="test") client = KworkClient(token="test")
respx.post(f"{BASE_URL}/isDialogAllow").mock( respx.post(f"{BASE_URL}/isDialogAllow").mock(
return_value=Response(200, json={"allowed": True}) return_value=Response(200, json={"allowed": True})
) )
result = await client.other.is_dialog_allow(12345) result = await client.other.is_dialog_allow(12345)
assert result is True assert result is True
@respx.mock @respx.mock
async def test_is_dialog_allow_false(self): async def test_is_dialog_allow_false(self):
"""Test is_dialog_allow returns False.""" """Test is_dialog_allow returns False."""
client = KworkClient(token="test") client = KworkClient(token="test")
respx.post(f"{BASE_URL}/isDialogAllow").mock( respx.post(f"{BASE_URL}/isDialogAllow").mock(
return_value=Response(200, json={"allowed": False}) return_value=Response(200, json={"allowed": False})
) )
result = await client.other.is_dialog_allow(67890) result = await client.other.is_dialog_allow(67890)
assert result is False assert result is False
@respx.mock @respx.mock
async def test_get_viewed_kworks(self): async def test_get_viewed_kworks(self):
"""Test getting viewed kworks.""" """Test getting viewed kworks."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"kworks": [ "kworks": [
{"id": 500, "title": "Viewed Kwork", "price": 1500.0}, {"id": 500, "title": "Viewed Kwork", "price": 1500.0},
] ]
} }
respx.post(f"{BASE_URL}/viewedCatalogKworks").mock( respx.post(f"{BASE_URL}/viewedCatalogKworks").mock(
return_value=Response(200, json=mock_data) return_value=Response(200, json=mock_data)
) )
result = await client.other.get_viewed_kworks() result = await client.other.get_viewed_kworks()
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 1 assert len(result) == 1
assert isinstance(result[0], Kwork) assert isinstance(result[0], Kwork)
@respx.mock @respx.mock
async def test_get_favorite_categories(self): async def test_get_favorite_categories(self):
"""Test getting favorite categories.""" """Test getting favorite categories."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"categories": [1, 5, 10, 15], "categories": [1, 5, 10, 15],
} }
respx.post(f"{BASE_URL}/favoriteCategories").mock( respx.post(f"{BASE_URL}/favoriteCategories").mock(
return_value=Response(200, json=mock_data) return_value=Response(200, json=mock_data)
) )
result = await client.other.get_favorite_categories() result = await client.other.get_favorite_categories()
assert isinstance(result, list) assert isinstance(result, list)
assert 1 in result assert 1 in result
assert 5 in result assert 5 in result
@respx.mock @respx.mock
async def test_update_settings(self): async def test_update_settings(self):
"""Test updating settings.""" """Test updating settings."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"success": True, "success": True,
"updated": {"notifications_enabled": False}, "updated": {"notifications_enabled": False},
} }
respx.post(f"{BASE_URL}/updateSettings").mock( respx.post(f"{BASE_URL}/updateSettings").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
settings = {"notifications_enabled": False, "theme": "dark"} settings = {"notifications_enabled": False, "theme": "dark"}
result = await client.other.update_settings(settings) result = await client.other.update_settings(settings)
assert result["success"] is True assert result["success"] is True
@respx.mock @respx.mock
async def test_go_offline(self): async def test_go_offline(self):
"""Test going offline.""" """Test going offline."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"success": True, "success": True,
"status": "offline", "status": "offline",
} }
respx.post(f"{BASE_URL}/offline").mock( respx.post(f"{BASE_URL}/offline").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.go_offline() result = await client.other.go_offline()
assert result["success"] is True assert result["success"] is True
assert result["status"] == "offline" assert result["status"] == "offline"
@respx.mock @respx.mock
async def test_get_actor(self): async def test_get_actor(self):
"""Test getting actor info.""" """Test getting actor info."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = { mock_data = {
"actor_id": 123, "actor_id": 123,
"name": "Test Actor", "name": "Test Actor",
} }
respx.post(f"{BASE_URL}/actor").mock( respx.post(f"{BASE_URL}/actor").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_actor() result = await client.other.get_actor()
assert result["actor_id"] == 123 assert result["actor_id"] == 123
class TestClientInternals: class TestClientInternals:
"""Tests for internal client methods.""" """Tests for internal client methods."""
def test_handle_response_success(self): def test_handle_response_success(self):
"""Test _handle_response with successful response.""" """Test _handle_response with successful response."""
import httpx import httpx
client = KworkClient(token="test") client = KworkClient(token="test")
response = httpx.Response(200, json={"success": True, "data": "test"}) response = httpx.Response(200, json={"success": True, "data": "test"})
result = client._handle_response(response) result = client._handle_response(response)
assert result["success"] is True assert result["success"] is True
assert result["data"] == "test" assert result["data"] == "test"
def test_handle_response_error(self): def test_handle_response_error(self):
"""Test _handle_response with error response.""" """Test _handle_response with error response."""
import httpx import httpx
client = KworkClient(token="test") client = KworkClient(token="test")
response = httpx.Response(400, json={"message": "Bad request"}) response = httpx.Response(400, json={"message": "Bad request"})
with pytest.raises(KworkApiError) as exc_info: with pytest.raises(KworkApiError) as exc_info:
client._handle_response(response) client._handle_response(response)
assert exc_info.value.status_code == 400 assert exc_info.value.status_code == 400
@respx.mock @respx.mock
async def test_request_method(self): async def test_request_method(self):
"""Test _request method directly.""" """Test _request method directly."""
client = KworkClient(token="test") client = KworkClient(token="test")
mock_data = {"result": "success"} mock_data = {"result": "success"}
respx.post(f"{BASE_URL}/test-endpoint").mock( respx.post(f"{BASE_URL}/test-endpoint").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client._request("POST", "/test-endpoint", json={"param": "value"}) result = await client._request("POST", "/test-endpoint", json={"param": "value"})
assert result["result"] == "success" assert result["result"] == "success"
async def test_context_manager_creates_client(self): async def test_context_manager_creates_client(self):
"""Test that context manager creates httpx client.""" """Test that context manager creates httpx client."""
async with KworkClient(token="test") as client: async with KworkClient(token="test") as client:
assert client.token == "test" assert client.token == "test"
assert client._client is None or client._client.is_closed assert client._client is None or client._client.is_closed