Обновить версию SDK до 2.1.19; добавить новые DTO для истории биллинга и упрощенных моделей провайдеров; переименовать и изменить существующие DTO для соответствия новому API

This commit is contained in:
Artem 2025-10-13 04:35:39 +02:00
parent 936c006b9b
commit 0a627eed81
No known key found for this signature in database
GPG key ID: 833485276B7902CE
7 changed files with 332 additions and 112 deletions

View file

@ -63,6 +63,7 @@ pip install git+https://github.com/remnawave/python-sdk.git@development
| Contract Version | Remnawave Panel Version |
| ---------------- | ----------------------- |
| 2.1.19 | >=2.1.19 |
| 2.1.18 | >=2.1.18 |
| 2.1.17 | >=2.1.16, <=2.1.17 |
| 2.1.16 | >=2.1.16 |

View file

@ -1,7 +1,7 @@
[project]
name = "remnawave"
version = "2.1.18"
description = "A Python SDK for interacting with the Remnawave API v2.1.18."
version = "2.1.19"
description = "A Python SDK for interacting with the Remnawave API v2.1.19."
authors = [
{name = "Artem",email = "dev@forestsnet.com"}
]

View file

@ -1,20 +1,21 @@
from typing import Annotated
from rapid_api_client import Path, Query
from rapid_api_client import Path
from rapid_api_client.annotations import PydanticBody
from remnawave.models import (
CreateInfraBillingHistoryRecordRequestDto,
CreateInfraBillingHistoryRecordResponseDto,
CreateInfraBillingNodeRequestDto,
CreateInfraBillingNodeResponseDto,
CreateInfraProviderRequestDto,
CreateInfraProviderResponseDto,
DeleteInfraBillingNodeResponseDto,
DeleteInfraProviderResponseDto,
GetAllInfraBillingHistoryResponseDto,
GetAllInfraBillingNodesResponseDto,
GetAllInfraProvidersResponseDto,
GetInfraBillingHistoryByUuidResponseDto,
GetInfraBillingNodeByUuidResponseDto,
DeleteInfraBillingHistoryRecordByUuidResponseDto,
DeleteInfraBillingNodeByUuidResponseDto,
DeleteInfraProviderByUuidResponseDto,
GetInfraBillingHistoryRecordsResponseDto,
GetInfraBillingNodesResponseDto,
GetInfraProvidersResponseDto,
GetInfraProviderByUuidResponseDto,
UpdateInfraBillingNodeRequestDto,
UpdateInfraBillingNodeResponseDto,
@ -25,8 +26,8 @@ from remnawave.rapid import BaseController, delete, get, patch, post
class InfraBillingController(BaseController):
@get("/infra-billing/providers", response_class=GetAllInfraProvidersResponseDto)
async def get_all_infra_providers(self) -> GetAllInfraProvidersResponseDto:
@get("/infra-billing/providers", response_class=GetInfraProvidersResponseDto)
async def get_infra_providers(self) -> GetInfraProvidersResponseDto:
"""Get all infra providers"""
...
@ -54,38 +55,38 @@ class InfraBillingController(BaseController):
"""Get infra provider by uuid"""
...
@delete("/infra-billing/providers/{uuid}", response_class=DeleteInfraProviderResponseDto)
@delete("/infra-billing/providers/{uuid}", response_class=DeleteInfraProviderByUuidResponseDto)
async def delete_infra_provider_by_uuid(
self,
uuid: Annotated[str, Path(description="UUID of the infra provider")],
) -> DeleteInfraProviderResponseDto:
"""Delete infra provider"""
) -> DeleteInfraProviderByUuidResponseDto:
"""Delete infra provider by uuid"""
...
@get("/infra-billing/history", response_class=GetAllInfraBillingHistoryResponseDto)
async def get_all_infra_billing_history(
@post("/infra-billing/history", response_class=CreateInfraBillingHistoryRecordResponseDto)
async def create_infra_billing_history_record(
self,
start: Annotated[int, Query(default=0, ge=0, description="Index to start pagination from")],
size: Annotated[int, Query(default=25, ge=1, description="Number of entries per page")],
) -> GetAllInfraBillingHistoryResponseDto:
"""Get all infra billing history"""
body: Annotated[CreateInfraBillingHistoryRecordRequestDto, PydanticBody()],
) -> CreateInfraBillingHistoryRecordResponseDto:
"""Create infra billing history"""
...
@get("/infra-billing/history/{uuid}", response_class=GetInfraBillingHistoryByUuidResponseDto)
async def get_infra_billing_history_by_uuid(
self,
uuid: Annotated[str, Path(description="UUID of the billing history entry")],
) -> GetInfraBillingHistoryByUuidResponseDto:
"""Get infra billing history by uuid"""
@get("/infra-billing/history", response_class=GetInfraBillingHistoryRecordsResponseDto)
async def get_infra_billing_history_records(self) -> GetInfraBillingHistoryRecordsResponseDto:
"""Get infra billing history"""
...
@get("/infra-billing/nodes", response_class=GetAllInfraBillingNodesResponseDto)
async def get_all_infra_billing_nodes(
@delete("/infra-billing/history/{uuid}", response_class=DeleteInfraBillingHistoryRecordByUuidResponseDto)
async def delete_infra_billing_history_record_by_uuid(
self,
start: Annotated[int, Query(default=0, ge=0, description="Index to start pagination from")],
size: Annotated[int, Query(default=25, ge=1, description="Number of entries per page")],
) -> GetAllInfraBillingNodesResponseDto:
"""Get all infra billing nodes"""
uuid: Annotated[str, Path(description="UUID of the billing history record")],
) -> DeleteInfraBillingHistoryRecordByUuidResponseDto:
"""Delete infra billing history"""
...
@get("/infra-billing/nodes", response_class=GetInfraBillingNodesResponseDto)
async def get_billing_nodes(self) -> GetInfraBillingNodesResponseDto:
"""Get infra billing nodes"""
...
@patch("/infra-billing/nodes", response_class=UpdateInfraBillingNodeResponseDto)
@ -93,7 +94,7 @@ class InfraBillingController(BaseController):
self,
body: Annotated[UpdateInfraBillingNodeRequestDto, PydanticBody()],
) -> UpdateInfraBillingNodeResponseDto:
"""Update infra billing node"""
"""Update infra billing nodes"""
...
@post("/infra-billing/nodes", response_class=CreateInfraBillingNodeResponseDto)
@ -104,18 +105,10 @@ class InfraBillingController(BaseController):
"""Create infra billing node"""
...
@get("/infra-billing/nodes/{uuid}", response_class=GetInfraBillingNodeByUuidResponseDto)
async def get_infra_billing_node_by_uuid(
self,
uuid: Annotated[str, Path(description="UUID of the infra billing node")],
) -> GetInfraBillingNodeByUuidResponseDto:
"""Get infra billing node by uuid"""
...
@delete("/infra-billing/nodes/{uuid}", response_class=DeleteInfraBillingNodeResponseDto)
@delete("/infra-billing/nodes/{uuid}", response_class=DeleteInfraBillingNodeByUuidResponseDto)
async def delete_infra_billing_node_by_uuid(
self,
uuid: Annotated[str, Path(description="UUID of the infra billing node")],
) -> DeleteInfraBillingNodeResponseDto:
) -> DeleteInfraBillingNodeByUuidResponseDto:
"""Delete infra billing node"""
...
...

View file

@ -98,15 +98,18 @@ from .inbounds_bulk_actions import (
RemoveInboundFromUsersResponseDto,
)
from .infra_billing import (
CreateInfraBillingHistoryRecordRequestDto,
CreateInfraBillingHistoryRecordResponseDto,
CreateInfraBillingNodeRequestDto,
CreateInfraBillingNodeResponseDto,
CreateInfraProviderRequestDto,
CreateInfraProviderResponseDto,
DeleteInfraBillingNodeResponseDto,
DeleteInfraProviderResponseDto,
GetAllInfraBillingHistoryResponseDto,
GetAllInfraBillingNodesResponseDto,
GetAllInfraProvidersResponseDto,
DeleteInfraBillingHistoryRecordByUuidResponseDto,
DeleteInfraBillingNodeByUuidResponseDto, # ПЕРЕИМЕНОВАНА (было DeleteInfraBillingNodeResponseDto)
DeleteInfraProviderByUuidResponseDto, # ПЕРЕИМЕНОВАНА (было DeleteInfraProviderResponseDto)
GetInfraBillingHistoryRecordsResponseDto, # ПЕРЕИМЕНОВАНА (было GetAllInfraBillingHistoryResponseDto)
GetInfraBillingNodesResponseDto, # ПЕРЕИМЕНОВАНА (было GetAllInfraBillingNodesResponseDto)
GetInfraProvidersResponseDto, # ПЕРЕИМЕНОВАНА (было GetAllInfraProvidersResponseDto)
GetInfraBillingHistoryByUuidResponseDto,
GetInfraBillingNodeByUuidResponseDto,
GetInfraProviderByUuidResponseDto,
@ -118,6 +121,11 @@ from .infra_billing import (
UpdateInfraBillingNodeResponseDto,
UpdateInfraProviderRequestDto,
UpdateInfraProviderResponseDto,
DeleteInfraBillingNodeResponseDto, # LEGACY
DeleteInfraProviderResponseDto, # LEGACY
GetAllInfraBillingHistoryResponseDto, # LEGACY
GetAllInfraBillingNodesResponseDto, # LEGACY
GetAllInfraProvidersResponseDto, # LEGACY
)
from .internal_squads import (
AddUsersToInternalSquadRequestDto,
@ -459,15 +467,18 @@ __all__ = [
"UpdateConfigProfileResponseDto",
"GetAllConfigProfilesResponsePaginated",
# Infra billing models
"CreateInfraBillingHistoryRecordRequestDto",
"CreateInfraBillingHistoryRecordResponseDto",
"CreateInfraBillingNodeRequestDto",
"CreateInfraBillingNodeResponseDto",
"CreateInfraProviderRequestDto",
"CreateInfraProviderResponseDto",
"DeleteInfraBillingNodeResponseDto",
"DeleteInfraProviderResponseDto",
"GetAllInfraBillingHistoryResponseDto",
"GetAllInfraBillingNodesResponseDto",
"GetAllInfraProvidersResponseDto",
"CreateInfraProviderResponseDto",
"DeleteInfraBillingHistoryRecordByUuidResponseDto",
"DeleteInfraBillingNodeByUuidResponseDto", # ПЕРЕИМЕНОВАНА (было DeleteInfraBillingNodeResponseDto)
"DeleteInfraProviderByUuidResponseDto", # ПЕРЕИМЕНОВАНА (было DeleteInfraProviderResponseDto)
"GetInfraBillingHistoryRecordsResponseDto", # ПЕРЕИМЕНОВАНА (было GetAllInfraBillingHistoryResponseDto)
"GetInfraBillingNodesResponseDto", # ПЕРЕИМЕНОВАНА (было GetAllInfraBillingNodesResponseDto)
"GetInfraProvidersResponseDto", # ПЕРЕИМЕНОВАНА (было GetAllInfraProvidersResponseDto)
"GetInfraBillingHistoryByUuidResponseDto",
"GetInfraBillingNodeByUuidResponseDto",
"GetInfraProviderByUuidResponseDto",
@ -479,6 +490,11 @@ __all__ = [
"UpdateInfraBillingNodeResponseDto",
"UpdateInfraProviderRequestDto",
"UpdateInfraProviderResponseDto",
"DeleteInfraBillingNodeResponseDto", # LEGACY
"DeleteInfraProviderResponseDto", # LEGACY
"GetAllInfraBillingHistoryResponseDto", # LEGACY
"GetAllInfraBillingNodesResponseDto", # LEGACY
"GetAllInfraProvidersResponseDto", # LEGACY
# Internal squads models
"AddUsersToInternalSquadRequestDto",
"AddUsersToInternalSquadResponseDto",

View file

@ -5,6 +5,27 @@ from uuid import UUID
from pydantic import BaseModel, Field
class InfraProviderSimpleDto(BaseModel):
"""Упрощенная модель провайдера для billingNodes"""
uuid: UUID
name: str
login_url: Optional[str] = Field(alias="loginUrl")
favicon_link: Optional[str] = Field(alias="faviconLink")
class InfraBillingHistoryStatsDto(BaseModel):
"""Статистика истории биллинга для провайдера"""
total_amount: float = Field(alias="totalAmount")
total_bills: float = Field(alias="totalBills")
class InfraBillingNodeSimpleDto(BaseModel):
"""Упрощенная модель узла биллинга для провайдера"""
node_uuid: UUID = Field(alias="nodeUuid")
name: str
country_code: str = Field(alias="countryCode")
class InfraProviderDto(BaseModel):
uuid: UUID
name: str
@ -12,17 +33,8 @@ class InfraProviderDto(BaseModel):
login_url: Optional[str] = Field(None, alias="loginUrl")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
class InfraBillingHistoryDto(BaseModel):
uuid: UUID
node_uuid: UUID = Field(alias="nodeUuid")
provider_uuid: UUID = Field(alias="providerUuid")
provider: InfraProviderDto
node: "NodeDto"
next_billing_at: datetime = Field(alias="nextBillingAt")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
billing_history: InfraBillingHistoryStatsDto = Field(alias="billingHistory")
billing_nodes: List[InfraBillingNodeSimpleDto] = Field(alias="billingNodes")
class NodeDto(BaseModel):
@ -31,17 +43,42 @@ class NodeDto(BaseModel):
country_code: str = Field(alias="countryCode")
class InfraBillingHistoryDto(BaseModel):
uuid: UUID
node_uuid: UUID = Field(alias="nodeUuid")
provider_uuid: UUID = Field(alias="providerUuid")
amount: float
description: Optional[str] = None
payment_date: datetime = Field(alias="paymentDate")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
class InfraBillingNodeDto(BaseModel):
uuid: UUID
node_uuid: UUID = Field(alias="nodeUuid")
provider_uuid: UUID = Field(alias="providerUuid")
provider: InfraProviderDto
provider: InfraProviderSimpleDto
node: NodeDto
next_billing_at: datetime = Field(alias="nextBillingAt")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
class AvailableBillingNodeDto(BaseModel):
"""Модель для доступных узлов биллинга"""
uuid: UUID
name: str
country_code: str = Field(alias="countryCode")
class BillingStatsDto(BaseModel):
"""Статистика биллинга"""
upcoming_nodes_count: float = Field(alias="upcomingNodesCount")
current_month_payments: float = Field(alias="currentMonthPayments")
total_spent: float = Field(alias="totalSpent")
# Provider models
class CreateInfraProviderRequestDto(BaseModel):
name: str
@ -65,11 +102,12 @@ class UpdateInfraProviderResponseDto(InfraProviderDto):
class AllInfraProvidersData(BaseModel):
total: int
total: float = Field(alias="total")
providers: List[InfraProviderDto]
class GetAllInfraProvidersResponseDto(AllInfraProvidersData):
# Исправленные имена моделей согласно OpenAPI
class GetInfraProvidersResponseDto(AllInfraProvidersData):
pass
@ -77,22 +115,35 @@ class GetInfraProviderByUuidResponseDto(InfraProviderDto):
pass
class DeleteInfraProviderResponseDto(BaseModel):
class DeleteInfraProviderByUuidResponseDto(BaseModel):
is_deleted: bool = Field(alias="isDeleted")
# Billing History models
class CreateInfraBillingHistoryRecordRequestDto(BaseModel):
"""Модель для создания записи истории биллинга"""
node_uuid: UUID = Field(serialization_alias="nodeUuid")
provider_uuid: UUID = Field(serialization_alias="providerUuid")
amount: float
description: Optional[str] = None
payment_date: datetime = Field(serialization_alias="paymentDate")
class CreateInfraBillingHistoryRecordResponseDto(InfraBillingHistoryDto):
pass
class InfraBillingHistoryData(BaseModel):
records: List[InfraBillingHistoryDto]
total: int
class GetAllInfraBillingHistoryResponseDto(InfraBillingHistoryData):
class GetInfraBillingHistoryRecordsResponseDto(InfraBillingHistoryData):
pass
class GetInfraBillingHistoryByUuidResponseDto(InfraBillingHistoryDto):
pass
class DeleteInfraBillingHistoryRecordByUuidResponseDto(BaseModel):
is_deleted: bool = Field(alias="isDeleted")
# Billing Nodes models
@ -102,15 +153,18 @@ class CreateInfraBillingNodeRequestDto(BaseModel):
next_billing_at: datetime = Field(serialization_alias="nextBillingAt")
class CreateInfraBillingNodeResponseDto(InfraBillingNodeDto):
pass
# ИСПРАВЛЕНО: API возвращает список всех billing nodes после создания, а не один созданный
class CreateInfraBillingNodeResponseDto(BaseModel):
total_billing_nodes: float = Field(alias="totalBillingNodes")
billing_nodes: List[InfraBillingNodeDto] = Field(alias="billingNodes")
available_billing_nodes: List[AvailableBillingNodeDto] = Field(alias="availableBillingNodes")
total_available_billing_nodes: float = Field(alias="totalAvailableBillingNodes")
stats: BillingStatsDto
class UpdateInfraBillingNodeRequestDto(BaseModel):
uuid: UUID
node_uuid: Optional[UUID] = Field(None, serialization_alias="nodeUuid")
provider_uuid: Optional[UUID] = Field(None, serialization_alias="providerUuid")
next_billing_at: Optional[datetime] = Field(None, serialization_alias="nextBillingAt")
uuids: List[UUID]
next_billing_at: datetime = Field(serialization_alias="nextBillingAt")
class UpdateInfraBillingNodeResponseDto(InfraBillingNodeDto):
@ -118,19 +172,31 @@ class UpdateInfraBillingNodeResponseDto(InfraBillingNodeDto):
class InfraBillingNodesData(BaseModel):
total_billing_nodes: int = Field(alias="totalBillingNodes")
total_active_nodes: Optional[int] = Field(None, alias="totalActiveNodes")
total_spent: Optional[int] = Field(None, alias="totalSpent")
total_billing_nodes: float = Field(alias="totalBillingNodes")
billing_nodes: List[InfraBillingNodeDto] = Field(alias="billingNodes")
available_billing_nodes: List[AvailableBillingNodeDto] = Field(alias="availableBillingNodes")
total_available_billing_nodes: float = Field(alias="totalAvailableBillingNodes")
stats: BillingStatsDto
class GetAllInfraBillingNodesResponseDto(InfraBillingNodesData):
class GetInfraBillingNodesResponseDto(InfraBillingNodesData):
pass
class GetInfraBillingNodeByUuidResponseDto(InfraBillingNodeDto):
pass
class DeleteInfraBillingNodeByUuidResponseDto(BaseModel):
"""API возвращает обновленный список billing nodes после удаления"""
total_billing_nodes: float = Field(alias="totalBillingNodes")
billing_nodes: List[InfraBillingNodeDto] = Field(alias="billingNodes")
available_billing_nodes: List[AvailableBillingNodeDto] = Field(alias="availableBillingNodes")
total_available_billing_nodes: float = Field(alias="totalAvailableBillingNodes")
stats: BillingStatsDto
class DeleteInfraBillingNodeResponseDto(BaseModel):
is_deleted: bool = Field(alias="isDeleted")
# Legacy aliases для обратной совместимости
GetAllInfraProvidersResponseDto = GetInfraProvidersResponseDto
DeleteInfraProviderResponseDto = DeleteInfraProviderByUuidResponseDto
GetAllInfraBillingHistoryResponseDto = GetInfraBillingHistoryRecordsResponseDto
GetInfraBillingHistoryByUuidResponseDto = InfraBillingHistoryDto
GetAllInfraBillingNodesResponseDto = GetInfraBillingNodesResponseDto
GetInfraBillingNodeByUuidResponseDto = InfraBillingNodeDto
DeleteInfraBillingNodeResponseDto = DeleteInfraBillingNodeByUuidResponseDto

View file

@ -292,7 +292,8 @@ class WebhookPayloadDto(BaseModel):
data = NodeDto(**data_raw)
elif event.startswith("service."):
if event.startswith("service.login_attempt"):
data = LoginAttemptDto(**data_raw)
login_attempt_data = data_raw.get("loginAttempt", {})
data = LoginAttemptDto(**login_attempt_data)
else: # service.panel_started - содержит пустой json
data = data_raw
elif event.startswith("errors."):

View file

@ -3,17 +3,18 @@ from datetime import datetime, timedelta
import pytest
from remnawave.models import (
CreateInfraBillingHistoryRecordRequestDto,
CreateInfraBillingHistoryRecordResponseDto,
CreateInfraBillingNodeRequestDto,
CreateInfraBillingNodeResponseDto,
CreateInfraProviderRequestDto,
CreateInfraProviderResponseDto,
DeleteInfraBillingNodeResponseDto,
DeleteInfraProviderResponseDto,
GetAllInfraBillingHistoryResponseDto,
GetAllInfraBillingNodesResponseDto,
GetAllInfraProvidersResponseDto,
GetInfraBillingHistoryByUuidResponseDto,
GetInfraBillingNodeByUuidResponseDto,
DeleteInfraBillingHistoryRecordByUuidResponseDto,
DeleteInfraBillingNodeByUuidResponseDto,
DeleteInfraProviderByUuidResponseDto,
GetInfraBillingHistoryRecordsResponseDto,
GetInfraBillingNodesResponseDto,
GetInfraProvidersResponseDto,
GetInfraProviderByUuidResponseDto,
UpdateInfraBillingNodeRequestDto,
UpdateInfraBillingNodeResponseDto,
@ -24,7 +25,8 @@ from tests.utils import generate_random_string
@pytest.mark.asyncio
async def test_infra_billing(remnawave) -> None:
async def test_infra_billing_providers(remnawave) -> None:
"""Test infra billing providers CRUD operations"""
provider_name = f"test_provider_{generate_random_string(length=6)}"
# Test create infra provider
@ -38,19 +40,26 @@ async def test_infra_billing(remnawave) -> None:
assert isinstance(create_provider, CreateInfraProviderResponseDto)
assert create_provider.name == provider_name
assert create_provider.favicon_link == "https://example.com/favicon.ico"
assert create_provider.login_url == "https://example.com/login"
provider_uuid = str(create_provider.uuid)
# Test get all infra providers
all_providers = await remnawave.infra_billing.get_all_infra_providers()
assert isinstance(all_providers, GetAllInfraProvidersResponseDto)
all_providers = await remnawave.infra_billing.get_infra_providers()
assert isinstance(all_providers, GetInfraProvidersResponseDto)
assert all_providers.total > 0
assert len(all_providers.providers) > 0
# Verify our provider is in the list
provider_found = any(p.uuid == create_provider.uuid for p in all_providers.providers)
assert provider_found
# Test get infra provider by uuid
provider_by_uuid = await remnawave.infra_billing.get_infra_provider_by_uuid(provider_uuid)
assert isinstance(provider_by_uuid, GetInfraProviderByUuidResponseDto)
assert provider_by_uuid.name == provider_name
assert provider_by_uuid.uuid == create_provider.uuid
# Test update infra provider
updated_name = f"updated_{provider_name}"
@ -58,25 +67,159 @@ async def test_infra_billing(remnawave) -> None:
UpdateInfraProviderRequestDto(
uuid=create_provider.uuid,
name=updated_name,
favicon_link="https://example.com/new-favicon.ico"
favicon_link="https://example.com/new-favicon.ico",
login_url="https://example.com/new-login"
)
)
assert isinstance(update_provider, UpdateInfraProviderResponseDto)
assert update_provider.name == updated_name
# Test get all infra billing history
billing_history = await remnawave.infra_billing.get_all_infra_billing_history(start=0, size=25)
assert isinstance(billing_history, GetAllInfraBillingHistoryResponseDto)
# Test get all infra billing nodes
billing_nodes = await remnawave.infra_billing.get_all_infra_billing_nodes(start=0, size=25)
assert isinstance(billing_nodes, GetAllInfraBillingNodesResponseDto)
# Skip testing actual billing node creation/update/delete as it requires existing nodes
# These would need real node UUIDs which may not exist in test environment
assert update_provider.favicon_link == "https://example.com/new-favicon.ico"
assert update_provider.login_url == "https://example.com/new-login"
# Test delete infra provider
delete_provider = await remnawave.infra_billing.delete_infra_provider_by_uuid(provider_uuid)
assert isinstance(delete_provider, DeleteInfraProviderResponseDto)
assert isinstance(delete_provider, DeleteInfraProviderByUuidResponseDto)
assert delete_provider.is_deleted is True
@pytest.mark.asyncio
async def test_infra_billing_history(remnawave) -> None:
"""Test infra billing history operations"""
# Test get all infra billing history
billing_history = await remnawave.infra_billing.get_infra_billing_history_records()
assert isinstance(billing_history, GetInfraBillingHistoryRecordsResponseDto)
assert hasattr(billing_history, 'records')
assert hasattr(billing_history, 'total')
# Skip creating history record as it may require specific setup
print("Billing history operations tested successfully")
@pytest.mark.asyncio
async def test_infra_billing_nodes(remnawave) -> None:
"""Test infra billing nodes operations"""
# Test get all infra billing nodes
billing_nodes = await remnawave.infra_billing.get_billing_nodes()
assert isinstance(billing_nodes, GetInfraBillingNodesResponseDto)
assert hasattr(billing_nodes, 'total_billing_nodes')
assert hasattr(billing_nodes, 'billing_nodes')
assert hasattr(billing_nodes, 'available_billing_nodes')
assert hasattr(billing_nodes, 'total_available_billing_nodes')
assert hasattr(billing_nodes, 'stats')
# Verify stats structure
assert hasattr(billing_nodes.stats, 'upcoming_nodes_count')
assert hasattr(billing_nodes.stats, 'current_month_payments')
assert hasattr(billing_nodes.stats, 'total_spent')
# Test create billing node (only if we have providers and available nodes)
providers = await remnawave.infra_billing.get_infra_providers()
if (providers.total > 0 and len(providers.providers) > 0 and
billing_nodes.total_available_billing_nodes > 0 and
len(billing_nodes.available_billing_nodes) > 0):
provider = providers.providers[0]
available_node = billing_nodes.available_billing_nodes[0]
next_billing = datetime.now() + timedelta(days=30)
# Create billing node - API возвращает весь список узлов
create_billing_node = await remnawave.infra_billing.create_infra_billing_node(
CreateInfraBillingNodeRequestDto(
node_uuid=available_node.uuid,
provider_uuid=provider.uuid,
next_billing_at=next_billing
)
)
assert isinstance(create_billing_node, CreateInfraBillingNodeResponseDto)
assert hasattr(create_billing_node, 'billing_nodes')
assert hasattr(create_billing_node, 'total_billing_nodes')
# Find the created billing node
created_node = None
for node in create_billing_node.billing_nodes:
if node.node.uuid == available_node.uuid and node.provider.uuid == provider.uuid:
created_node = node
break
assert created_node is not None, "Created billing node not found in response"
billing_node_uuid = str(created_node.uuid)
# Test delete billing node - API возвращает обновленный список
delete_billing_node = await remnawave.infra_billing.delete_infra_billing_node_by_uuid(billing_node_uuid)
assert isinstance(delete_billing_node, DeleteInfraBillingNodeByUuidResponseDto)
assert hasattr(delete_billing_node, 'billing_nodes')
assert hasattr(delete_billing_node, 'total_billing_nodes')
# Verify the node was deleted (not in the response list)
node_still_exists = any(
node.uuid == created_node.uuid
for node in delete_billing_node.billing_nodes
)
assert not node_still_exists, "Billing node should be deleted but still found in response"
@pytest.mark.asyncio
async def test_infra_billing_complete_workflow(remnawave) -> None:
"""Test complete workflow: create provider -> create billing node -> cleanup"""
provider_name = f"workflow_provider_{generate_random_string(length=6)}"
# 1. Create provider
create_provider = await remnawave.infra_billing.create_infra_provider(
CreateInfraProviderRequestDto(
name=provider_name,
favicon_link="https://workflow.com/favicon.ico",
login_url="https://workflow.com/login"
)
)
assert isinstance(create_provider, CreateInfraProviderResponseDto)
provider_uuid = str(create_provider.uuid)
try:
# 2. Get available nodes
billing_nodes = await remnawave.infra_billing.get_billing_nodes()
if (billing_nodes.total_available_billing_nodes > 0 and
len(billing_nodes.available_billing_nodes) > 0):
available_node = billing_nodes.available_billing_nodes[0]
next_billing = datetime.now() + timedelta(days=30)
# 3. Create billing node
create_billing_node = await remnawave.infra_billing.create_infra_billing_node(
CreateInfraBillingNodeRequestDto(
node_uuid=available_node.uuid,
provider_uuid=create_provider.uuid,
next_billing_at=next_billing
)
)
# Find created node
created_node = None
for node in create_billing_node.billing_nodes:
if node.node.uuid == available_node.uuid and node.provider.uuid == create_provider.uuid:
created_node = node
break
assert created_node is not None
billing_node_uuid = str(created_node.uuid)
# 4. Cleanup billing node - API возвращает обновленный список
delete_billing_node = await remnawave.infra_billing.delete_infra_billing_node_by_uuid(billing_node_uuid)
assert isinstance(delete_billing_node, DeleteInfraBillingNodeByUuidResponseDto)
# Verify deletion by checking the node is not in the list
node_still_exists = any(
node.uuid == created_node.uuid
for node in delete_billing_node.billing_nodes
)
assert not node_still_exists, "Billing node should be deleted"
finally:
# 5. Cleanup provider
delete_provider = await remnawave.infra_billing.delete_infra_provider_by_uuid(provider_uuid)
assert delete_provider.is_deleted is True